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
29 changes: 3 additions & 26 deletions Bookie/UI/Coordinator.swift → Bookie/DI/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@

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"
private var searchText = "C"

func set(window: UIWindow) {
self.window = window
Expand All @@ -38,15 +32,15 @@ class Coordinator: AnyCoordinator {
}

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

@MainActor
fileprivate class func createDetailScreen(
class func createDetailScreen(
_ data: Book
) -> (AnyBookScreen & UIViewController) {
BookScreen(data)
Expand All @@ -61,20 +55,3 @@ 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)
}
}
26 changes: 26 additions & 0 deletions Bookie/DI/CoordinatorSwiftUI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// CoordinatorSwiftUI.swift
// Bookie
//
// Created by Roman Podymov on 27/04/2025.
// Copyright © 2025 Bookie. All rights reserved.
//

import UIKit

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

@MainActor
override class func createDetailScreen(
_ data: Book
) -> (AnyBookScreen & UIViewController) {
BookScreenSwiftUI(data)
}
}
53 changes: 53 additions & 0 deletions Bookie/DI/DI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// DI.swift
// Bookie
//
// Created by Roman Podymov on 27/04/2025.
// Copyright © 2025 Bookie. All rights reserved.
//

import Fashion
import SwiftData
@preconcurrency import Swinject
import Then
import UIKit

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

protocol RemoteDataSource {
func search(text: String) async throws (BooksViewModelError) -> BookResponse
}

protocol LocalDataSource: RemoteDataSource {
func save(books: [Book]) async throws (BooksViewModelError)
}

extension Container: @retroactive Then {}

let dependenciesContainer = Container().then { result in
let objectScope: ObjectScope = .container
result.register(AnyCoordinator.self) { _ in
CoordinatorSwiftUI()
}.inObjectScope(objectScope)
result.register(Stylesheet.self) { _ in
MainStylesheet()
}.inObjectScope(objectScope)
result.register(RemoteDataSource.self) { _ in
GoogleRemoteDataSource()
}.inObjectScope(objectScope)
result.register(LocalDataSource.self) { _ in
if #available(iOS 17, *), let container = {
let configuration = ModelConfiguration(for: BookSwiftData.self)
let schema = Schema([BookSwiftData.self])
return try? ModelContainer(for: schema, configurations: [configuration])
}() {
SwiftDataSource(modelContainer: container)
} else {
RealmDataSource()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@
import Foundation
import Moya

protocol RemoteDataSource {
func search(text: String) async throws (BooksViewModelError) -> BookResponse
}

protocol LocalDataSource: RemoteDataSource {
func save(books: [Book]) async throws (BooksViewModelError)
}

struct GoogleRemoteDataSource: RemoteDataSource {
func search(text: String) async throws (BooksViewModelError) -> BookResponse {
let provider = MoyaProvider<BooksService>(plugins: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
import Foundation
import RealmSwift

final class BookImageRealm: Object {
@Persisted var smallThumbnail: String?
@Persisted var thumbnail: String?
@Persisted var small: String?
@Persisted var medium: String?
@Persisted var large: String?
@Persisted var extraLarge: String?
}

final class BookRealm: Object, Identifiable {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var image: BookImageRealm?
}

struct RealmDataSource: LocalDataSource {
Expand Down Expand Up @@ -40,18 +50,25 @@ struct RealmDataSource: LocalDataSource {
categories: nil,
averageRating: nil,
ratingsCount: nil,
imageLinks: nil,
imageLinks: .init(
smallThumbnail: $0.image?.smallThumbnail,
thumbnail: $0.image?.thumbnail,
small: $0.image?.small,
medium: $0.image?.medium,
large: $0.image?.large,
extraLarge: $0.image?.extraLarge
),
language: "cs"
),
saleInfo: nil,
accessInfo: nil,
searchInfo: nil
)
}
let booksForTitle = books.filter { book in
let booksForTitle = Array(books.filter { book in
book.volumeInfo.title.contains(text)
}
return BookResponse(kind: "", totalItems: booksForTitle.count, items: Array(booksForTitle))
})
return BookResponse(kind: "", totalItems: booksForTitle.count, items: booksForTitle)
}.value
} catch {
throw BooksViewModelError.noData
Expand All @@ -64,10 +81,19 @@ struct RealmDataSource: LocalDataSource {
let realm = try await Realm()
for book in books {
try realm.write {
let obj = BookRealm()
obj.id = book.id
obj.title = book.volumeInfo.title
realm.add(obj)
let obj = BookRealm().then {
$0.id = book.id
$0.title = book.volumeInfo.title
$0.image = BookImageRealm().then {
$0.smallThumbnail = book.volumeInfo.imageLinks?.smallThumbnail
$0.thumbnail = book.volumeInfo.imageLinks?.thumbnail
$0.small = book.volumeInfo.imageLinks?.small
$0.medium = book.volumeInfo.imageLinks?.medium
$0.large = book.volumeInfo.imageLinks?.large
$0.extraLarge = book.volumeInfo.imageLinks?.extraLarge
}
}
realm.add(obj, update: .modified)
}
}
}.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import SwiftData

@available(iOS 17, *)
@Model
final class BookSwiftData {
var id: String
final class BookSwiftData: Identifiable {
@Attribute(.unique) var id: String
var title: String

init(id: String, title: String) {
Expand Down
28 changes: 0 additions & 28 deletions Bookie/UI/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,9 @@
//

import Fashion
import SwiftData
@preconcurrency import Swinject
import Then
import UIKit

let dependenciesContainer = {
let result = Container()
let objectScope: ObjectScope = .container
result.register(AnyCoordinator.self) { _ in
CoordinatorSwiftUI()
}.inObjectScope(objectScope)
result.register(Stylesheet.self) { _ in
MainStylesheet()
}.inObjectScope(objectScope)
result.register(RemoteDataSource.self) { _ in
GoogleRemoteDataSource()
}.inObjectScope(objectScope)
result.register(LocalDataSource.self) { _ in
if #available(iOS 17, *), let container = {
let configuration = ModelConfiguration(for: BookSwiftData.self)
let schema = Schema([BookSwiftData.self])
return try? ModelContainer(for: schema, configurations: [configuration])
}() {
return SwiftDataSource(modelContainer: container)
} else {
return RealmDataSource()
}
}
return result
}()

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
Expand Down
12 changes: 7 additions & 5 deletions Bookie/UI/BookScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ final class BookScreen: UIViewController {
make.leading.top.trailing.bottom.equalToSuperview()
}
}
bookImage.kf.setImage(
with: .network(
URL(
unsafeString: viewModel.data?.volumeInfo.imageLinks?.detailScreenImage ?? ""
if let image = viewModel.data?.volumeInfo.imageLinks?.detailScreenImage {
bookImage.kf.setImage(
with: .network(
URL(
unsafeString: image
)
)
)
)
}

setupButtons()

Expand Down
8 changes: 4 additions & 4 deletions Bookie/UI/BookScreenSwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ final class BookScreenSwiftUI: UIHostingController<BookScreenRootView>, AnyBookS
viewModel.screen = self
}

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

private static func backPressedBinding(viewModel: BookViewModel) -> Binding<Bool> {
.init(get: {
false
Expand All @@ -82,8 +86,4 @@ final class BookScreenSwiftUI: UIHostingController<BookScreenRootView>, AnyBookS
}
})
}

@MainActor @preconcurrency dynamic required init?(coder _: NSCoder) {
nil
}
}
10 changes: 2 additions & 8 deletions Bookie/UI/BooksScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,8 @@ final class BooksScreen: UIViewController {
}
}

Task { [weak loadingView, weak viewModel] in
loadingView?.show()

await viewModel?.reloadData()
}
loadingView.show()
viewModel.ready()
}
}

Expand Down Expand Up @@ -162,9 +159,6 @@ extension BooksScreen: AnyBooksScreen {
}

func onSearchTextChanged(_ searchText: String) async {
if searchBar == nil {
return
}
await MainActor.run { [weak searchBar] in
searchBar?.text = searchText
}
Expand Down
27 changes: 15 additions & 12 deletions Bookie/UI/BooksScreenSwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,35 @@ final class BooksScreenSwiftUI: UIHostingController<BooksScreenRootView>, AnyBoo
viewModel = .init(screen: nil, searchText: searchText, previousBook: previousBook)
super.init(
rootView: BooksScreenRootView(
selectedBook: Self.selectedBook(searchText: searchText, previousBook: previousBook),
selectedBook: Self.selectedBook(
previousBook: previousBook,
viewModel: viewModel
),
searchText: Self.searchTextBinding(viewModel: viewModel)
)
)
viewModel.screen = self
viewModel.ready()
}

Task { [weak viewModel] in
await viewModel?.reloadData()
}
@MainActor @preconcurrency dynamic required init?(coder _: NSCoder) {
nil
}

private static func selectedBook(searchText: String, previousBook: Book?) -> Binding<Book?> {
private static func selectedBook(
previousBook: Book?,
viewModel: BooksViewModel
) -> Binding<Book?> {
.init(get: {
previousBook
}, set: { book in
}, set: { [weak viewModel] book in
guard let book else {
return
}
Task {
Task { [weak viewModel] in
await dependenciesContainer.resolve(
AnyCoordinator.self
)?.openDetailScreen(book, searchText: searchText)
)?.openDetailScreen(book, searchText: viewModel?.searchText.value ?? "")
}
})
}
Expand All @@ -124,8 +131,4 @@ final class BooksScreenSwiftUI: UIHostingController<BooksScreenRootView>, AnyBoo
await viewModel?.reloadData()
}.value
}

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