Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Bookie/UI/Extensions.swift → Bookie/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import UIKit

extension Optional {
func mapAsync<E, U>(_ transform: @Sendable (Wrapped) async throws (E) -> U) async throws (E) -> U? where E: Error, U: ~Copyable {
func mapAsync<E, U>(
_ transform: @Sendable (Wrapped) async throws (E) -> U
) async throws (E) -> U? where E: Error, U: ~Copyable {
if let self {
return try await transform(self)
} else {
Expand Down
File renamed without changes.
8 changes: 1 addition & 7 deletions Bookie/UI/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,10 @@ import Fashion
import Then
import UIKit

protocol AnyCoordinator {
func set(window: UIWindow)
func openHomeScreen(previousBook: Book?) async
func openDetailScreen(_ data: Book, searchText: String) async
}

let dependenciesContainer = {
let result = Container()
result.register(AnyCoordinator.self) { _ in
Coordinator()
CoordinatorSwiftUI()
}.inObjectScope(.container)
result.register(Stylesheet.self) { _ in
MainStylesheet()
Expand Down
89 changes: 89 additions & 0 deletions Bookie/UI/BookScreenSwiftUI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// BookScreenSwiftUI.swift
// Bookie
//
// Created by Roman Podymov on 25/04/2025.
// Copyright © 2025 Bookie. All rights reserved.
//

import Kingfisher
import SwiftUI

struct BookScreenRootView: View {
@Binding var backPressed: Bool
@State var data: Book?

var body: some View {
ZStack {
if let detailScreenImage = data?.volumeInfo.imageLinks?.detailScreenImage {
KFImage(URL(
unsafeString: detailScreenImage
))
.resizable()
.aspectRatio(contentMode: .fill)
}
VStack {
HStack {
Button {
backPressed = true
} label: {
Image(systemName: "arrowshape.backward.fill")
}
Spacer()
}
.padding(.leading, LayoutParams.BooksScren.defaultInset)
.padding(.top, LayoutParams.BooksScren.defaultInset)
Spacer()
HStack {
VStack(alignment: .leading) {
Text(data?.volumeInfo.title ?? "")
.foregroundStyle(Color(AppColors.textColor))
Text(data?.volumeInfo.authors?.joined(separator: ", ") ?? "")
.foregroundStyle(Color(AppColors.textColor))
Text(data?.volumeInfo.publishedDate ?? "")
.foregroundStyle(Color(AppColors.textColor))
Text(data?.volumeInfo.description ?? "")
.foregroundStyle(Color(AppColors.textColor))
}
.background(Color(AppColors.metadataBackgroundColor))
Spacer()
}
.padding(.leading, LayoutParams.BooksScren.defaultInset)
.padding(.bottom, LayoutParams.BooksScren.defaultInset)
}
}
}
}

final class BookScreenSwiftUI: UIHostingController<BookScreenRootView>, AnyBookScreen {
private var viewModel: BookViewModel!

init(_ data: Book?) {
viewModel = BookViewModel(screen: nil, data: data)
super.init(
rootView: BookScreenRootView(
backPressed: Self.backPressedBinding(viewModel: viewModel),
data: data
)
)
viewModel.screen = self
}

private static func backPressedBinding(viewModel: BookViewModel) -> Binding<Bool> {
.init(get: {
false
}, set: { _ in
Task { [weak viewModel] in
await dependenciesContainer.resolve(
AnyCoordinator.self
)?.openHomeScreen(
previousBook: viewModel?.data
)
}
})
}

@MainActor @preconcurrency dynamic required init?(coder _: NSCoder) {
nil
}
}
9 changes: 5 additions & 4 deletions Bookie/UI/BooksScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ final class BooksScreen: UIViewController {
$0.apply(styles: Style.booksScreenRootView)
$0.dataSource = self
$0.delegate = self
($0.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing = LayoutParams.BooksScren.smallerInset
($0.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumInteritemSpacing = LayoutParams.BooksScren.smallerInset
($0.collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize = LayoutParams.BooksScren.itemSize
($0.collectionViewLayout as? UICollectionViewFlowLayout)?.headerReferenceSize = LayoutParams.BooksScren.sectionHeaderSize
let collectionViewLayout = $0.collectionViewLayout as? UICollectionViewFlowLayout
collectionViewLayout?.minimumLineSpacing = LayoutParams.BooksScren.smallerInset
collectionViewLayout?.minimumInteritemSpacing = LayoutParams.BooksScren.smallerInset
collectionViewLayout?.itemSize = LayoutParams.BooksScren.itemSize
collectionViewLayout?.headerReferenceSize = LayoutParams.BooksScren.sectionHeaderSize
$0.register(
supplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withClass: BookSectionHeader.self
Expand Down
115 changes: 106 additions & 9 deletions Bookie/UI/BooksScreenSwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,126 @@
// Copyright © 2025 Bookie. All rights reserved.
//

import OrderedCollections
import SwiftUI

// TODO: SwiftUI version
extension Book: Identifiable {}

final class BooksScreenRootViewState: ObservableObject {
@Published var data: DataSetType = .init()
}

extension DataSetItemType: @retroactive Identifiable {
public var id: DataSetKeyType {
differenceIdentifier
}
}

struct BooksScreenRootView: View {
@ObservedObject var state: BooksScreenRootViewState
@Binding private var selectedBook: Book?
@Binding private var searchText: String
@State private var showCancelButton = false

init(
state: BooksScreenRootViewState = BooksScreenRootViewState(),
selectedBook: Binding<Book?>,
searchText: Binding<String>
) {
self.state = state
_selectedBook = selectedBook
_searchText = searchText
}

var body: some View {
Text("Hello")
VStack {
HStack {
TextField("", text: $searchText, onEditingChanged: { _ in
self.showCancelButton = true
}, onCommit: {})
.padding(.leading, LayoutParams.BooksScren.defaultInset)
.foregroundStyle(Color(AppColors.textColor))
if showCancelButton {
Button(L10n.BooksScreen.Button.cancel) {
self.searchText = ""
self.showCancelButton = false
}
.foregroundStyle(Color(AppColors.textColor))
.padding(.trailing, LayoutParams.BooksScren.defaultInset)
}
}
List {
ForEach(state.data) { section in
Section(header: Text(section.model.joined(separator: ", "))) {
ForEach(section.elements) { book in
Button(action: {
selectedBook = book
}, label: {
Text(book.volumeInfo.title)
.foregroundStyle(Color(AppColors.textColor))
})
}
}
}
}
}
}
}

final class BooksScreenSwiftUI: UIHostingController<BooksScreenRootView>, AnyBooksScreen {
func onNewDataReceived(oldSet _: DataSetType, newSet _: DataSetType) async {}
private var viewModel: BooksViewModel!

func onNewDataError(_: BooksViewModelError) async {}
init(searchText: String, previousBook: Book?) {
viewModel = .init(screen: nil, searchText: searchText, previousBook: previousBook)
super.init(
rootView: BooksScreenRootView(
selectedBook: Self.selectedBook(searchText: searchText, previousBook: previousBook),
searchText: Self.searchTextBinding(viewModel: viewModel)
)
)
viewModel.screen = self

func onSearchTextChanged(_: String) async {}
Task { [weak viewModel] in
await viewModel?.reloadData()
}
}

private static func selectedBook(searchText: String, previousBook: Book?) -> Binding<Book?> {
.init(get: {
previousBook
}, set: { book in
guard let book else {
return
}
Task {
await dependenciesContainer.resolve(
AnyCoordinator.self
)?.openDetailScreen(book, searchText: searchText)
}
})
}

private static func searchTextBinding(viewModel: BooksViewModel) -> Binding<String> {
.init(get: { [weak viewModel] in
viewModel?.searchText.value ?? ""
}, set: { [weak viewModel] in
viewModel?.searchText.send($0)
})
}

init() {
super.init(rootView: BooksScreenRootView())
func onNewDataReceived(oldSet _: DataSetType, newSet: DataSetType) async {
rootView.state.data = newSet
}

func onNewDataError(_: BooksViewModelError) async {}

func onSearchTextChanged(_: String) async {
await Task { [weak viewModel] in
await viewModel?.reloadData()
}.value
}

@MainActor @preconcurrency dynamic required init?(coder _: NSCoder) {
nil
}

func onNewDataReceived() async {}
}
48 changes: 43 additions & 5 deletions Bookie/UI/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

import UIKit

protocol AnyCoordinator {
func set(window: UIWindow)
func openHomeScreen(previousBook: Book?) async
func openDetailScreen(_ data: Book, searchText: String) async
}

class Coordinator: AnyCoordinator {
private weak var window: UIWindow?
private var searchText = "Karel"
Expand All @@ -18,20 +24,35 @@ class Coordinator: AnyCoordinator {

func openHomeScreen(previousBook: Book?) async {
await MainActor.run { [weak window, searchText] in
window?.rootViewController = BooksScreen(searchText: searchText, previousBook: previousBook)
window?.rootViewController = Self.createRootScreen(searchText: searchText, previousBook: previousBook)
}
await animateScrenChange()
await animateScreenChange()
}

func openDetailScreen(_ data: Book, searchText: String) async {
self.searchText = searchText
await MainActor.run { [weak window] in
window?.rootViewController = BookScreen(data)
window?.rootViewController = Self.createDetailScreen(data)
}
await animateScrenChange()
await animateScreenChange()
}

@MainActor
fileprivate class func createRootScreen(
searchText: String,
previousBook: Book?
) -> (AnyBooksScreen & UIViewController) {
BooksScreen(searchText: searchText, previousBook: previousBook)
}

@MainActor
fileprivate class func createDetailScreen(
_ data: Book
) -> (AnyBookScreen & UIViewController) {
BookScreen(data)
}

private func animateScrenChange() async {
fileprivate func animateScreenChange() async {
guard let window else {
return
}
Expand All @@ -40,3 +61,20 @@ class Coordinator: AnyCoordinator {
await UIView.transition(with: window, duration: duration, options: options, animations: {})
}
}

class CoordinatorSwiftUI: Coordinator {
@MainActor
override fileprivate class func createRootScreen(
searchText: String,
previousBook: Book?
) -> (AnyBooksScreen & UIViewController) {
BooksScreenSwiftUI(searchText: searchText, previousBook: previousBook)
}

@MainActor
override fileprivate class func createDetailScreen(
_ data: Book
) -> (AnyBookScreen & UIViewController) {
BookScreenSwiftUI(data)
}
}
Loading