From 6b7d67d2e00e07ab252341115b3cf878c3122fd5 Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Fri, 30 Aug 2019 14:53:27 -0300 Subject: [PATCH] feat(bookslist): adding books list screen feature --- iOSBooks/Podfile | 1 + iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 104 +++++++++++++++--- iOSBooks/iOSBooks/AppDelegate.swift | 19 +++- iOSBooks/iOSBooks/Base.lproj/Main.storyboard | 24 ---- .../BooksList/View/BooksList.storyboard | 73 ++++++++++++ ...oksListViewController+CollectionView.swift | 23 ++++ .../View/BooksListViewController+View.swift | 30 +++++ .../View/BooksListViewController.swift | 22 ++++ .../BooksList/View/Subviews/BooksCell.swift | 30 +++++ .../ViewModel/BooksListViewModel.swift | 57 ++++++++++ .../AppCoordinator/AppCoordinator.swift | 29 +++++ .../AppCoordinatorDependencyInjector.swift | 29 +++++ .../iOSBooks/Data/Clients/BooksClient.swift | 4 +- iOSBooks/iOSBooks/Identifiable.swift | 71 ++++++++++++ iOSBooks/iOSBooks/Info.plist | 9 +- .../UIViewController+QuickInstance.swift | 47 ++++++++ iOSBooks/iOSBooks/ViewController.swift | 15 --- 17 files changed, 523 insertions(+), 64 deletions(-) delete mode 100644 iOSBooks/iOSBooks/Base.lproj/Main.storyboard create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift create mode 100644 iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift create mode 100644 iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift create mode 100644 iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift create mode 100644 iOSBooks/iOSBooks/Identifiable.swift create mode 100644 iOSBooks/iOSBooks/UIViewController+QuickInstance.swift delete mode 100644 iOSBooks/iOSBooks/ViewController.swift diff --git a/iOSBooks/Podfile b/iOSBooks/Podfile index 21ec943..a563546 100644 --- a/iOSBooks/Podfile +++ b/iOSBooks/Podfile @@ -8,6 +8,7 @@ target 'iOSBooks' do # Pods for iOSBooks pod 'PromiseKit' pod 'IQKeyboardManagerSwift' + pod 'Kingfisher', "~>5.1" target 'iOSBooksTests' do inherit! :search_paths diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index 2d57161..ed6db10 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964D72314BB470093738B /* AppDelegate.swift */; }; - 0E3964DA2314BB470093738B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964D92314BB470093738B /* ViewController.swift */; }; - 0E3964DD2314BB470093738B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964DB2314BB470093738B /* Main.storyboard */; }; 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964DE2314BB4A0093738B /* Assets.xcassets */; }; 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */; }; 0E3964ED2314BB4A0093738B /* iOSBooksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964EC2314BB4A0093738B /* iOSBooksTests.swift */; }; @@ -24,6 +22,16 @@ 7447D36C2316F12800E01BD3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36B2316F12800E01BD3 /* Item.swift */; }; 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */; }; 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */; }; + 7447D37A2316F71200E01BD3 /* BooksListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */; }; + 7447D37C2316F71D00E01BD3 /* BooksListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */; }; + 7447D37E2316F72A00E01BD3 /* BooksList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */; }; + 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */; }; + 74ADE9BE23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */; }; + 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */; }; + 74ADE9C223196E99006D2644 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C123196E99006D2644 /* Identifiable.swift */; }; + 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C42319730F006D2644 /* BooksCell.swift */; }; + 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */; }; + 74ADE9C9231981F5006D2644 /* BooksListViewController+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */; }; 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */; }; /* End PBXBuildFile section */ @@ -40,8 +48,6 @@ /* Begin PBXFileReference section */ 0E3964D42314BB470093738B /* iOSBooks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSBooks.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3964D72314BB470093738B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 0E3964D92314BB470093738B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 0E3964DC2314BB470093738B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 0E3964DE2314BB4A0093738B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0E3964E12314BB4A0093738B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 0E3964E32314BB4A0093738B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -63,6 +69,16 @@ 7447D36B2316F12800E01BD3 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLinks.swift; sourceTree = ""; }; 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SalesInfo.swift; sourceTree = ""; }; + 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksListViewModel.swift; sourceTree = ""; }; + 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksListViewController.swift; sourceTree = ""; }; + 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BooksList.storyboard; sourceTree = ""; }; + 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorDependencyInjector.swift; sourceTree = ""; }; + 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+QuickInstance.swift"; sourceTree = ""; }; + 74ADE9C123196E99006D2644 /* Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; + 74ADE9C42319730F006D2644 /* BooksCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksCell.swift; sourceTree = ""; }; + 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+CollectionView.swift"; sourceTree = ""; }; + 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+View.swift"; sourceTree = ""; }; E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -109,13 +125,15 @@ 0E3964D62314BB470093738B /* iOSBooks */ = { isa = PBXGroup; children = ( + 74ADE9B5231969F0006D2644 /* Coordinators */, + 7447D3762316F6C700E01BD3 /* BooksList */, 7447D3712316F16E00E01BD3 /* Data */, 0E3964D72314BB470093738B /* AppDelegate.swift */, - 0E3964D92314BB470093738B /* ViewController.swift */, - 0E3964DB2314BB470093738B /* Main.storyboard */, 0E3964DE2314BB4A0093738B /* Assets.xcassets */, 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */, 0E3964E32314BB4A0093738B /* Info.plist */, + 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */, + 74ADE9C123196E99006D2644 /* Identifiable.swift */, ); path = iOSBooks; sourceTree = ""; @@ -187,6 +205,60 @@ path = Clients; sourceTree = ""; }; + 7447D3762316F6C700E01BD3 /* BooksList */ = { + isa = PBXGroup; + children = ( + 7447D3782316F6D700E01BD3 /* View */, + 7447D3772316F6CF00E01BD3 /* ViewModel */, + ); + path = BooksList; + sourceTree = ""; + }; + 7447D3772316F6CF00E01BD3 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 7447D3782316F6D700E01BD3 /* View */ = { + isa = PBXGroup; + children = ( + 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */, + 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */, + 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */, + 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */, + 74ADE9C3231972ED006D2644 /* Subviews */, + ); + path = View; + sourceTree = ""; + }; + 74ADE9B5231969F0006D2644 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 74ADE9B6231969F7006D2644 /* AppCoordinator */, + ); + path = Coordinators; + sourceTree = ""; + }; + 74ADE9B6231969F7006D2644 /* AppCoordinator */ = { + isa = PBXGroup; + children = ( + 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */, + 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */, + ); + path = AppCoordinator; + sourceTree = ""; + }; + 74ADE9C3231972ED006D2644 /* Subviews */ = { + isa = PBXGroup; + children = ( + 74ADE9C42319730F006D2644 /* BooksCell.swift */, + ); + path = Subviews; + sourceTree = ""; + }; E4186846F20EA500EF61D605 /* Pods */ = { isa = PBXGroup; children = ( @@ -284,8 +356,8 @@ buildActionMask = 2147483647; files = ( 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */, + 7447D37E2316F72A00E01BD3 /* BooksList.storyboard in Resources */, 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */, - 0E3964DD2314BB470093738B /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -384,12 +456,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 74ADE9C9231981F5006D2644 /* BooksListViewController+View.swift in Sources */, + 74ADE9C223196E99006D2644 /* Identifiable.swift in Sources */, 7447D36C2316F12800E01BD3 /* Item.swift in Sources */, 7447D3682316E57500E01BD3 /* Book.swift in Sources */, + 74ADE9BE23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift in Sources */, 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */, + 7447D37A2316F71200E01BD3 /* BooksListViewModel.swift in Sources */, 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */, - 0E3964DA2314BB470093738B /* ViewController.swift in Sources */, + 7447D37C2316F71D00E01BD3 /* BooksListViewController.swift in Sources */, + 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */, + 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */, + 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */, 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */, + 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */, 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */, 7447D35E2316C91700E01BD3 /* APIClient.swift in Sources */, 7447D3642316E02500E01BD3 /* Endpoint.swift in Sources */, @@ -418,14 +498,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 0E3964DB2314BB470093738B /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 0E3964DC2314BB470093738B /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/iOSBooks/iOSBooks/AppDelegate.swift b/iOSBooks/iOSBooks/AppDelegate.swift index cc5fb9d..4e75ef8 100644 --- a/iOSBooks/iOSBooks/AppDelegate.swift +++ b/iOSBooks/iOSBooks/AppDelegate.swift @@ -6,16 +6,27 @@ // Copyright © 2019 Guilherme Antunes. All rights reserved. // +import IQKeyboardManagerSwift import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - - + var coordinator: AppCoordinator? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + + window = UIWindow(frame: UIScreen.main.bounds) + guard let window = window else { + print("window is unexpectedly nil") + return false + } + IQKeyboardManager.shared.enable = true + IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false + coordinator = AppCoordinator(window: window) + coordinator?.start() + return true } @@ -38,7 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + window = nil } diff --git a/iOSBooks/iOSBooks/Base.lproj/Main.storyboard b/iOSBooks/iOSBooks/Base.lproj/Main.storyboard deleted file mode 100644 index f1bcf38..0000000 --- a/iOSBooks/iOSBooks/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard new file mode 100644 index 0000000..511c79e --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift new file mode 100644 index 0000000..7ac7c72 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift @@ -0,0 +1,23 @@ +// +// BooksListViewController+CollectionView.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +extension BooksListViewController: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel?.numberOfItemsInSection() ?? 0 + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return viewModel?.numberOfSections() ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + return viewModel?.cellForItem(inCollectionView: collectionView, atIndexPath: indexPath) ?? UICollectionViewCell() + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift new file mode 100644 index 0000000..e2ccd98 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift @@ -0,0 +1,30 @@ +// +// BooksListViewController+View.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol BooksListViewControllerPresentable { + func reloadView() + func presentError(message: String) +} + +extension BooksListViewController: BooksListViewControllerPresentable { + func reloadView() { + booksCollectionView?.reloadData() + } + + func presentError(message: String) { + alert(message: message) + } + + func setupCollectionView() { + title = "iOS Books" + booksCollectionView?.dataSource = self + booksCollectionView?.delegate = self + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift new file mode 100644 index 0000000..7661e23 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift @@ -0,0 +1,22 @@ +// +// BooksListViewController.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class BooksListViewController: UIViewController { + + // MARK: - Properties + var viewModel: BooksListViewModelProtocol? + @IBOutlet weak var booksCollectionView: UICollectionView? + + // MARK: - View Life Cycle + override func viewDidLoad() { + setupCollectionView() + viewModel?.loadBooks() + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift new file mode 100644 index 0000000..da5e82c --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift @@ -0,0 +1,30 @@ +// +// BooksCell.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Kingfisher +import UIKit + +class BooksCell: UICollectionViewCell { + @IBOutlet weak var bookImageView: UIImageView? + + func setup(withBook book: Book?) { + if let book = book, let bookURLString = book.imageLinks?.thumbnail, let url = URL(string: bookURLString) { + bookImageView?.kf.setImage(with: url) + } + } + + override func prepareForReuse() { + super.prepareForReuse() + bookImageView?.kf.cancelDownloadTask() + } + + deinit { + bookImageView?.kf.cancelDownloadTask() + } + +} diff --git a/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift new file mode 100644 index 0000000..d913009 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift @@ -0,0 +1,57 @@ +// +// BooksListViewModel.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol BooksListViewModelProtocol { + func loadBooks() + func numberOfSections() -> Int + func numberOfItemsInSection() -> Int + func cellForItem(inCollectionView collectionView: UICollectionView, atIndexPath indexPath: IndexPath) -> UICollectionViewCell +} + +class BooksListViewModel: BooksListViewModelProtocol { + + var items: [Item] = [] + var service = BooksClient() + var startingIndex = 0 + var view: BooksListViewControllerPresentable? + + func numberOfSections() -> Int { + return 1 + } + + func numberOfItemsInSection() -> Int { + return items.count + } + + func cellForItem(inCollectionView collectionView: UICollectionView, atIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let cell: BooksCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setup(withBook: items[indexPath.item].book) + if indexPath.item == (items.count - 1) { + loadBooks() + } + return cell + } + + func loadBooks() { + service.fetchBooksList(startingIndex: startingIndex).done { [weak self] (list) in + guard let self = self, let items = list.items else { return } + self.items.append(contentsOf: items) + self.startingIndex = items.count + DispatchQueue.main.async { + self.view?.reloadView() + } + }.catch { (error) in + DispatchQueue.main.async { + self.view?.presentError(message: error.localizedDescription) + } + } + + } +} diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift new file mode 100644 index 0000000..f0129ef --- /dev/null +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift @@ -0,0 +1,29 @@ +// +// AppCoordinator.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class AppCoordinator { + + lazy var injector = AppCoordinatorDependencyInjector() + var window: UIWindow + + init(window: UIWindow) { + self.window = window + } + + func start() { + setupNavigationController() + window.rootViewController = injector.navigationController + window.makeKeyAndVisible() + } + + func setupNavigationController() { + injector.navigationController.viewControllers.append(injector.booksListViewController) + } +} diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift new file mode 100644 index 0000000..4ed7ff9 --- /dev/null +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift @@ -0,0 +1,29 @@ +// +// AppCoordinatorDependencyInjector.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class AppCoordinatorDependencyInjector { + lazy var navigationController: UINavigationController = { + let navigation = UINavigationController() + navigation.navigationBar.prefersLargeTitles = true + navigation.navigationBar.barStyle = .black + return navigation + }() + + lazy var booksListViewController: BooksListViewController = { + let controller: BooksListViewController = BooksListViewController.instantiate() + booksListViewModel.view = controller + controller.viewModel = booksListViewModel + return controller + }() + + lazy var booksListViewModel: BooksListViewModel = { + return BooksListViewModel() + }() +} diff --git a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift index c541b85..224d3e3 100644 --- a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift +++ b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift @@ -17,7 +17,7 @@ class BooksClient { self.apiClient = apiClient } - func fetchBooksList() -> Promise { - return apiClient.request(model: BooksList.self, BooksAPI.list(startingIndex: 0).request) + func fetchBooksList(startingIndex index: Int) -> Promise { + return apiClient.request(model: BooksList.self, BooksAPI.list(startingIndex: index).request) } } diff --git a/iOSBooks/iOSBooks/Identifiable.swift b/iOSBooks/iOSBooks/Identifiable.swift new file mode 100644 index 0000000..a3ff390 --- /dev/null +++ b/iOSBooks/iOSBooks/Identifiable.swift @@ -0,0 +1,71 @@ +// +// Identifiable.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol Identifiable: class {} + +extension Identifiable where Self: UIViewController { + static var storyboardIdentifier: String { + return String(describing: self) + } + static var xibIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UITableViewCell { + static var reuseIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UICollectionViewCell { + static var reuseIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UITableViewHeaderFooterView { + static var reuseIdentifier: String { + return String(describing: self) + } +} + + +extension UICollectionView { + func dequeueReusableCell(for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + // TODO: set crashlytics to warn here + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return cell + } +} + +extension UITableView { + func dequeueReusableCell(for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return cell + } + + func dequeueReusableHeaderFooterView() -> T { + guard let headerFooterView = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return headerFooterView + } +} + +extension UICollectionViewCell: Identifiable {} +extension UITableViewCell: Identifiable {} +extension UIViewController: Identifiable {} +extension UITableViewHeaderFooterView: Identifiable {} + diff --git a/iOSBooks/iOSBooks/Info.plist b/iOSBooks/iOSBooks/Info.plist index 16be3b6..1c45a97 100644 --- a/iOSBooks/iOSBooks/Info.plist +++ b/iOSBooks/iOSBooks/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -23,7 +28,7 @@ UILaunchStoryboardName LaunchScreen UIMainStoryboardFile - Main + BooksList UIRequiredDeviceCapabilities armv7 @@ -31,8 +36,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift new file mode 100644 index 0000000..3ccb0ed --- /dev/null +++ b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift @@ -0,0 +1,47 @@ +// +// UIViewController+QuickInstance.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// +import UIKit + +extension UIViewController { + + static func instantiate() -> T { + guard let controller = UIStoryboard(name: T.storyboardIdentifier.replacingOccurrences(of: "Controller", with: "").replacingOccurrences(of: "View", with: ""), bundle: T.bundle).instantiateViewController(withIdentifier: T.storyboardIdentifier) as? T else { + fatalError("failed to create storyboard")} + return controller + } + + static func instantiateFromXIB() -> T { + return T(nibName: T.xibIdentifier.replacingOccurrences(of: "Controller", with: "").replacingOccurrences(of: "View", with: ""), bundle: .main) + } + + static var bundle: Bundle { + return Bundle(for: self) + } +} + +extension UIViewController { + + func setBackButton(_ backFunction: Selector) { +// navigationItem.hidesBackButton = true +// let newBackButton = UIBarButtonItem(image: UIImage(named: "back_arrow"), style: .plain, target: self, action: backFunction) +// newBackButton.title = "Voltar" +// navigationItem.leftBarButtonItem = newBackButton + } + +} + +extension UIViewController { + + func alert(title: String = "", message: String, completion: (() -> Void)? = nil, okActionHandler: ((UIAlertAction) -> Void)? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let OKAction = UIAlertAction(title: "OK", style: .default, handler: okActionHandler) + alertController.addAction(OKAction) + present(alertController, animated: true, completion: completion) + } + +} diff --git a/iOSBooks/iOSBooks/ViewController.swift b/iOSBooks/iOSBooks/ViewController.swift deleted file mode 100644 index 9d3dfa9..0000000 --- a/iOSBooks/iOSBooks/ViewController.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ViewController.swift -// iOSBooks -// -// Created by Guilherme Antunes on 26/08/19. -// Copyright © 2019 Guilherme Antunes. All rights reserved. -// - -import UIKit -import PromiseKit - -class ViewController: UIViewController { - -} -