├── .DS_Store ├── .gitignore ├── CleanArchitecture.png ├── LICENSE ├── Library.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Library ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Core │ ├── Entities │ │ └── Book.swift │ ├── Gateways │ │ └── BooksGateway.swift │ ├── Result.swift │ └── UseCases │ │ ├── AddBook.swift │ │ ├── DeleteBook.swift │ │ └── DisplayBooksList.swift ├── EntityGateway │ ├── API │ │ ├── ApiClient.swift │ │ ├── ApiResponse.swift │ │ ├── Entities │ │ │ └── ApiBook.swift │ │ ├── Gateways │ │ │ └── ApiBooksGateway.swift │ │ ├── JSON.swift │ │ └── Requests │ │ │ ├── AddBookApiRequest.swift │ │ │ ├── BooksApiRequest.swift │ │ │ └── DeleteBookApiRequest.swift │ ├── Cache │ │ └── CacheBooksGateway.swift │ └── LocalPersistence │ │ ├── CoreDataStack.swift │ │ ├── Entities │ │ └── CoreDataBook.swift │ │ ├── Gateways │ │ └── LocalPersistenceBooksGateway.swift │ │ ├── Library.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── Library.xcdatamodel │ │ │ └── contents │ │ └── NSManagedObjectContext-Utils.swift ├── Info.plist ├── Other │ ├── Date-RelativeDescription.swift │ ├── UITextField-EmptyText.swift │ ├── UIViewController-Alert.swift │ └── ViewRouter.swift └── Scenes │ ├── AddBook │ ├── AddBookConfigurator.swift │ ├── AddBookPresenter.swift │ ├── AddBookViewController.swift │ └── AddBookViewRouter.swift │ ├── BookDetails │ ├── BookDetailsConfigurator.swift │ ├── BookDetailsPresenter.swift │ ├── BookDetailsTableViewController.swift │ └── BookDetailsViewRouter.swift │ └── Books │ ├── BookTableViewCell.swift │ ├── BooksConfigurator.swift │ ├── BooksPresenter.swift │ ├── BooksTableViewController.swift │ └── BooksViewRouter.swift ├── LibraryTests ├── Gateways │ ├── ApiClientTest.swift │ ├── CacheBooksGatewayTest.swift │ └── CoreDataBooksGatewayTest.swift ├── Helpers │ ├── Creators │ │ ├── AddBookParameters.swift │ │ ├── Book.swift │ │ └── NSError.swift │ ├── Gateways │ │ ├── ApiBooksGatewaySpy.swift │ │ ├── BooksGatewaySpy.swift │ │ └── LocalPersistenceBooksGatewaySpy.swift │ ├── InMemoryCoreDataStack.swift │ ├── NSManagedObjectContextSpy.swift │ ├── Presenters │ │ └── AddBookPresenterStub.swift │ ├── Routers │ │ ├── AddBookViewRouterSpy.swift │ │ └── BooksViewRouterSpy.swift │ ├── URLSessionStub.swift │ ├── UseCases │ │ ├── DeleteBookUseCaseSpy.swift │ │ └── DisplayBooksUseCaseStub.swift │ └── Views │ │ ├── BookCellViewSpy.swift │ │ └── BooksViewSpy.swift ├── Info.plist ├── Presenters │ └── BooksPresenterTest.swift ├── Result.swift └── UseCases │ └── DeleteBookUseCaseTest.swift └── readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FortechRomania/ios-mvp-clean-architecture/654d3e9d1cd6ca3a53402dd91acf84435ceef259/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Library.xcodeproj/xcuserdata 2 | Library.xcodeproj/project.xcworkspace/xcuserdata -------------------------------------------------------------------------------- /CleanArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FortechRomania/ios-mvp-clean-architecture/654d3e9d1cd6ca3a53402dd91acf84435ceef259/CleanArchitecture.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fortech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Library.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Library.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Library/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | import CoreData 30 | 31 | @UIApplicationMain 32 | class AppDelegate: UIResponder, UIApplicationDelegate { 33 | 34 | var window: UIWindow? 35 | var coreDataStack = CoreDataStackImplementation.sharedInstance 36 | 37 | 38 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 39 | // Override point for customization after application launch. 40 | return true 41 | } 42 | 43 | func applicationWillResignActive(_ application: UIApplication) { 44 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 45 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 46 | } 47 | 48 | func applicationDidEnterBackground(_ application: UIApplication) { 49 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 50 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 51 | } 52 | 53 | func applicationWillEnterForeground(_ application: UIApplication) { 54 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 55 | } 56 | 57 | func applicationDidBecomeActive(_ application: UIApplication) { 58 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 59 | } 60 | 61 | func applicationWillTerminate(_ application: UIApplication) { 62 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 63 | // Saves changes in the application's managed object context before the application terminates. 64 | coreDataStack.saveContext() 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Library/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Library/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Library/Core/Entities/Book.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Book.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | struct Book { 31 | var id: String 32 | var isbn: String 33 | var title: String 34 | var author: String 35 | var releaseDate: Date? 36 | var pages: Int 37 | 38 | var durationToReadInHours: Double { 39 | // Let's pretend it takes one hour to read 30 pages. 40 | // This is what we would usually call business logic - that is logic that is "true" across multiple applications 41 | // It's true however that usually this would be returned by the API as most of the business logic usually sits on the API side 42 | return Double(pages) / 30.0 43 | } 44 | } 45 | 46 | extension Book: Equatable { } 47 | 48 | func == (lhs: Book, rhs: Book) -> Bool { 49 | return lhs.id == rhs.id 50 | } 51 | -------------------------------------------------------------------------------- /Library/Core/Gateways/BooksGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksGateway.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | typealias FetchBooksEntityGatewayCompletionHandler = (_ books: Result<[Book]>) -> Void 31 | typealias AddBookEntityGatewayCompletionHandler = (_ books: Result) -> Void 32 | typealias DeleteBookEntityGatewayCompletionHandler = (_ books: Result) -> Void 33 | 34 | 35 | protocol BooksGateway { 36 | func fetchBooks(completionHandler: @escaping FetchBooksEntityGatewayCompletionHandler) 37 | func add(parameters: AddBookParameters, completionHandler: @escaping AddBookEntityGatewayCompletionHandler) 38 | func delete(book: Book, completionHandler: @escaping DeleteBookEntityGatewayCompletionHandler) 39 | } 40 | -------------------------------------------------------------------------------- /Library/Core/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | struct CoreError: Error { 31 | var localizedDescription: String { 32 | return message 33 | } 34 | 35 | var message = "" 36 | } 37 | 38 | typealias Result = Swift.Result 39 | -------------------------------------------------------------------------------- /Library/Core/UseCases/AddBook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookUseCase.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | typealias AddBookUseCaseCompletionHandler = (_ books: Result) -> Void 31 | 32 | protocol AddBookUseCase { 33 | func add(parameters: AddBookParameters, completionHandler: @escaping AddBookUseCaseCompletionHandler) 34 | } 35 | 36 | // This class is used across all layers - Core, UI and Network 37 | // It's not violating any dependency rules. 38 | // However it might make sense for each layer do define it's own input parameters so it can be used independently of the other layers. 39 | struct AddBookParameters { 40 | var isbn: String 41 | var title: String 42 | var author: String 43 | var releaseDate: Date? 44 | var pages: Int 45 | } 46 | 47 | class AddBookUseCaseImplementation: AddBookUseCase { 48 | let booksGateway: BooksGateway 49 | 50 | init(booksGateway: BooksGateway) { 51 | self.booksGateway = booksGateway 52 | } 53 | 54 | // MARK: - DeleteBookUseCase 55 | 56 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 57 | self.booksGateway.add(parameters: parameters) { (result) in 58 | // Do any additional processing & after that call the completion handler 59 | completionHandler(result) 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Library/Core/UseCases/DeleteBook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteBook.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | typealias DeleteBookUseCaseCompletionHandler = (_ books: Result) -> Void 31 | 32 | struct DeleteBookUseCaseNotifications { 33 | // Notification sent when a book is deleted having the book set to the notification object 34 | static let didDeleteBook = Notification.Name("didDeleteBookNotification") 35 | } 36 | 37 | protocol DeleteBookUseCase { 38 | 39 | func delete(book: Book, completionHandler: @escaping DeleteBookUseCaseCompletionHandler) 40 | } 41 | 42 | class DeleteBookUseCaseImplementation: DeleteBookUseCase { 43 | 44 | let booksGateway: BooksGateway 45 | 46 | init(booksGateway: BooksGateway) { 47 | self.booksGateway = booksGateway 48 | } 49 | 50 | // MARK: - DeleteBookUseCase 51 | 52 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 53 | self.booksGateway.delete(book: book) { (result) in 54 | // Do any additional processing & after that call the completion handler 55 | // In this case we will broadcast a notification 56 | switch result { 57 | case .success(): 58 | NotificationCenter.default.post(name: DeleteBookUseCaseNotifications.didDeleteBook, object: book) 59 | completionHandler(result) 60 | case .failure(_): 61 | completionHandler(result) 62 | } 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Library/Core/UseCases/DisplayBooksList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBooksList.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | typealias DisplayBooksUseCaseCompletionHandler = (_ books: Result<[Book]>) -> Void 31 | 32 | protocol DisplayBooksUseCase { 33 | func displayBooks(completionHandler: @escaping DisplayBooksUseCaseCompletionHandler) 34 | } 35 | 36 | class DisplayBooksListUseCaseImplementation: DisplayBooksUseCase { 37 | let booksGateway: BooksGateway 38 | 39 | init(booksGateway: BooksGateway) { 40 | self.booksGateway = booksGateway 41 | } 42 | 43 | // MARK: - DisplayBooksUseCase 44 | 45 | func displayBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 46 | self.booksGateway.fetchBooks { (result) in 47 | // Do any additional processing & after that call the completion handler 48 | completionHandler(result) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/ApiClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiClient.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol ApiRequest { 31 | var urlRequest: URLRequest { get } 32 | } 33 | 34 | protocol ApiClient { 35 | func execute(request: ApiRequest, completionHandler: @escaping (_ result: Result>) -> Void) 36 | } 37 | 38 | protocol URLSessionProtocol { 39 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask 40 | } 41 | 42 | extension URLSession: URLSessionProtocol { } 43 | 44 | class ApiClientImplementation: ApiClient { 45 | let urlSession: URLSessionProtocol 46 | 47 | init(urlSessionConfiguration: URLSessionConfiguration, completionHandlerQueue: OperationQueue) { 48 | urlSession = URLSession(configuration: urlSessionConfiguration, delegate: nil, delegateQueue: completionHandlerQueue) 49 | } 50 | 51 | // This should be used mainly for testing purposes 52 | init(urlSession: URLSessionProtocol) { 53 | self.urlSession = urlSession 54 | } 55 | 56 | // MARK: - ApiClient 57 | 58 | func execute(request: ApiRequest, completionHandler: @escaping (Result>) -> Void) { 59 | let dataTask = urlSession.dataTask(with: request.urlRequest) { (data, response, error) in 60 | guard let httpUrlResponse = response as? HTTPURLResponse else { 61 | completionHandler(.failure(NetworkRequestError(error: error))) 62 | return 63 | } 64 | 65 | let successRange = 200...299 66 | if successRange.contains(httpUrlResponse.statusCode) { 67 | do { 68 | let response = try ApiResponse(data: data, httpUrlResponse: httpUrlResponse) 69 | completionHandler(.success(response)) 70 | } catch { 71 | completionHandler(.failure(error)) 72 | } 73 | } else { 74 | completionHandler(.failure(ApiError(data: data, httpUrlResponse: httpUrlResponse))) 75 | } 76 | } 77 | dataTask.resume() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/ApiResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiResponse.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // Can be thrown when we can't even reach the API 31 | struct NetworkRequestError: Error { 32 | let error: Error? 33 | 34 | var localizedDescription: String { 35 | return error?.localizedDescription ?? "Network request error - no other information" 36 | } 37 | } 38 | 39 | // Can be thrown when we reach the API but the it returns a 4xx or a 5xx 40 | struct ApiError: Error { 41 | let data: Data? 42 | let httpUrlResponse: HTTPURLResponse 43 | } 44 | 45 | // Can be thrown by InitializableWithData.init(data: Data?) implementations when parsing the data 46 | struct ApiParseError: Error { 47 | static let code = 999 48 | 49 | let error: Error 50 | let httpUrlResponse: HTTPURLResponse 51 | let data: Data? 52 | 53 | var localizedDescription: String { 54 | return error.localizedDescription 55 | } 56 | } 57 | 58 | // This wraps a successful API response and it includes the generic data as well 59 | // The reason why we need this wrapper is that we want to pass to the client the status code and the raw response as well 60 | struct ApiResponse { 61 | let entity: T 62 | let httpUrlResponse: HTTPURLResponse 63 | let data: Data? 64 | 65 | init(data: Data?, httpUrlResponse: HTTPURLResponse) throws { 66 | do { 67 | self.entity = try JSONDecoder().decode(T.self, from: data ?? Data()) 68 | self.httpUrlResponse = httpUrlResponse 69 | self.data = data 70 | } catch { 71 | throw ApiParseError(error: error, httpUrlResponse: httpUrlResponse, data: data) 72 | } 73 | } 74 | } 75 | 76 | // Some endpoints might return a 204 No Content 77 | struct VoidResponse: Decodable { } 78 | 79 | extension NSError { 80 | static func createPraseError() -> NSError { 81 | return NSError(domain: "com.fortech.library", 82 | code: ApiParseError.code, 83 | userInfo: [NSLocalizedDescriptionKey: "A parsing error occured"]) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/Entities/ApiBook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiBook.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // If your company develops the API then it's relatively safe to have a single representation 31 | // for both the API entities and your core entities. So depending on the complexity of your app this entity might be an overkill 32 | struct ApiBook: Decodable { 33 | var id: String 34 | var isbn: String 35 | var title: String 36 | var author: String 37 | var releaseDate: Date? 38 | var pages: Int 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case id = "Id" 42 | case isbn = "ISBN" 43 | case title = "Title" 44 | case author = "Author" 45 | case releaseDate = "Pages" 46 | case pages = "ReleaseDate" 47 | } 48 | } 49 | 50 | extension ApiBook { 51 | var book: Book { 52 | return Book(id: id, 53 | isbn: isbn, 54 | title: title, 55 | author: author, 56 | releaseDate: releaseDate, 57 | pages: pages) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/Gateways/ApiBooksGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiBooksGateway.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // This protocol in not necessarily needed since it doesn't include any extra methods 31 | // besides what BooksGateway already provides. However, if there would be any extra methods 32 | // on the API that we would need to support it would make sense to have an API specific gateway protocol 33 | protocol ApiBooksGateway: BooksGateway { 34 | 35 | } 36 | 37 | class ApiBooksGatewayImplementation: ApiBooksGateway { 38 | let apiClient: ApiClient 39 | 40 | init(apiClient: ApiClient) { 41 | self.apiClient = apiClient 42 | } 43 | 44 | // MARK: - ApiBooksGateway 45 | 46 | func fetchBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 47 | let booksApiRequest = BooksApiRequest() 48 | apiClient.execute(request: booksApiRequest) { (result: Result>) in 49 | switch result { 50 | case let .success(response): 51 | let books = response.entity.map { return $0.book } 52 | completionHandler(.success(books)) 53 | case let .failure(error): 54 | completionHandler(.failure(error)) 55 | } 56 | } 57 | } 58 | 59 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 60 | let addBookApiRequest = AddBookApiRequest(addBookParameters: parameters) 61 | apiClient.execute(request: addBookApiRequest) { (result: Result>) in 62 | switch result { 63 | case let .success(response): 64 | let book = response.entity.book 65 | completionHandler(.success(book)) 66 | case let .failure(error): 67 | completionHandler(.failure(error)) 68 | } 69 | } 70 | } 71 | 72 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 73 | let deleteBookApiRequest = DeleteBookApiRequest(bookId: book.id) 74 | apiClient.execute(request: deleteBookApiRequest) { (result: Result>) in 75 | switch result { 76 | case .success(_): 77 | completionHandler(.success(())) 78 | case let .failure(error): 79 | completionHandler(.failure(error)) 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | /** 31 | Extension on `Dictionary` that adds different helper methods such as JSON `Data` serialization 32 | */ 33 | public extension Dictionary where Key: ExpressibleByStringLiteral, Value: Any { 34 | 35 | /** 36 | Heper method that serializes the `Dictionary` to JSON `Data` 37 | 38 | - returns: `Data` containing the serialized JSON or empty `Data` (e.g. `Data()`) if the serialization fails 39 | */ 40 | func toJsonData() -> Data { 41 | do { 42 | return try JSONSerialization.data(withJSONObject: self, options: []) 43 | } catch { 44 | return Data() 45 | } 46 | } 47 | } 48 | 49 | /** 50 | Extension on `Array` that adds different helper methods such as JSON `Data` serialization 51 | */ 52 | public extension Array where Element: Any { 53 | /** 54 | Heper method that serializes the `Array` to JSON `Data` 55 | 56 | - returns: `Data` containing the serialized JSON or empty `Data` (e.g. `Data()`) if the serialization fails 57 | */ 58 | func toJsonData() -> Data { 59 | do { 60 | return try JSONSerialization.data(withJSONObject: self, options: []) 61 | } catch { 62 | return Data() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/Requests/AddBookApiRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookApiRequest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // Dummy implementation. The endpoint doesn't exist 31 | struct AddBookApiRequest: ApiRequest { 32 | let addBookParameters: AddBookParameters 33 | 34 | var urlRequest: URLRequest { 35 | let url: URL! = URL(string: "https://api.library.fortech.ro/books") 36 | var request = URLRequest(url: url) 37 | 38 | request.httpMethod = "POST" 39 | 40 | request.setValue("application/vnd.fortech.book-creation+json", forHTTPHeaderField: "Content-Type") 41 | request.setValue("application/vnd.fortech.book+json", forHTTPHeaderField: "Accept") 42 | 43 | request.httpBody = addBookParameters.toJsonData() 44 | 45 | return request 46 | } 47 | } 48 | 49 | extension AddBookParameters { 50 | func toJsonData() -> Data { 51 | var dictionary = [String: Any]() 52 | 53 | dictionary["ISBN"] = isbn 54 | dictionary["Title"] = title 55 | dictionary["Author"] = author 56 | dictionary["Pages"] = pages 57 | 58 | // Normally this should be formatted to a standard such as ISO8601 59 | dictionary["ReleaseDate"] = releaseDate?.timeIntervalSinceNow 60 | 61 | return dictionary.toJsonData() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/Requests/BooksApiRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksApiRequest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // Dummy implementation. The endpoint doesn't exist. 31 | struct BooksApiRequest: ApiRequest { 32 | var urlRequest: URLRequest { 33 | let url: URL! = URL(string: "https://api.library.fortech.ro/books") 34 | var request = URLRequest(url: url) 35 | 36 | request.setValue("application/vnd.fortech.books-list+json", forHTTPHeaderField: "Accept") 37 | 38 | request.httpMethod = "GET" 39 | 40 | return request 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Library/EntityGateway/API/Requests/DeleteBookApiRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteBookApiRequest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // Dummy implementation. The endpoint doesn't exist. 31 | struct DeleteBookApiRequest: ApiRequest { 32 | let bookId: String 33 | 34 | var urlRequest: URLRequest { 35 | let url: URL! = URL(string: "https://api.library.fortech.ro/books/\(bookId)") 36 | var request = URLRequest(url: url) 37 | 38 | request.httpMethod = "DELETE" 39 | 40 | return request 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Library/EntityGateway/Cache/CacheBooksGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheBooksGateway.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // Discussion: 31 | // Maybe it makes sense to perform all the operations locally and only after that make the API call 32 | // to sync the local content with the API. 33 | // If that's the case you will only have to change this class and the use case won't be impacted 34 | class CacheBooksGateway: BooksGateway { 35 | let apiBooksGateway: ApiBooksGateway 36 | let localPersistenceBooksGateway: LocalPersistenceBooksGateway 37 | 38 | init(apiBooksGateway: ApiBooksGateway, localPersistenceBooksGateway: LocalPersistenceBooksGateway) { 39 | self.apiBooksGateway = apiBooksGateway 40 | self.localPersistenceBooksGateway = localPersistenceBooksGateway 41 | } 42 | 43 | // MARK: - BooksGateway 44 | 45 | func fetchBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 46 | apiBooksGateway.fetchBooks { (result) in 47 | self.handleFetchBooksApiResult(result, completionHandler: completionHandler) 48 | } 49 | } 50 | 51 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 52 | apiBooksGateway.add(parameters: parameters) { (result) in 53 | self.handleAddBookApiResult(result, parameters: parameters, completionHandler: completionHandler) 54 | } 55 | } 56 | 57 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 58 | apiBooksGateway.delete(book: book) { (result) in 59 | self.localPersistenceBooksGateway.delete(book: book, completionHandler: completionHandler) 60 | } 61 | } 62 | 63 | // MARK: - Private 64 | 65 | fileprivate func handleFetchBooksApiResult(_ result: Result<[Book]>, completionHandler: @escaping (Result<[Book]>) -> Void) { 66 | switch result { 67 | case let .success(books): 68 | localPersistenceBooksGateway.save(books: books) 69 | completionHandler(result) 70 | case .failure(_): 71 | localPersistenceBooksGateway.fetchBooks(completionHandler: completionHandler) 72 | } 73 | } 74 | 75 | fileprivate func handleAddBookApiResult(_ result: Result, parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 76 | switch result { 77 | case let .success(book): 78 | self.localPersistenceBooksGateway.add(book: book) 79 | completionHandler(result) 80 | case .failure(_): 81 | self.localPersistenceBooksGateway.add(parameters: parameters, completionHandler: completionHandler) 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/CoreDataStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataStack.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | protocol CoreDataStack { 32 | var persistentContainer: NSPersistentContainer { get } 33 | func saveContext() 34 | } 35 | 36 | class CoreDataStackImplementation { 37 | 38 | static let sharedInstance = CoreDataStackImplementation() 39 | 40 | // MARK: - Core Data stack 41 | 42 | lazy var persistentContainer: NSPersistentContainer = { 43 | /* 44 | The persistent container for the application. This implementation 45 | creates and returns a container, having loaded the store for the 46 | application to it. This property is optional since there are legitimate 47 | error conditions that could cause the creation of the store to fail. 48 | */ 49 | let container = NSPersistentContainer(name: "Library") 50 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 51 | if let error = error as NSError? { 52 | // Replace this implementation with code to handle the error appropriately. 53 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 54 | 55 | /* 56 | Typical reasons for an error here include: 57 | * The parent directory does not exist, cannot be created, or disallows writing. 58 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 59 | * The device is out of space. 60 | * The store could not be migrated to the current model version. 61 | Check the error message to determine what the actual problem was. 62 | */ 63 | fatalError("Unresolved error \(error), \(error.userInfo)") 64 | } 65 | }) 66 | return container 67 | }() 68 | 69 | // MARK: - Core Data Saving support 70 | 71 | func saveContext () { 72 | let context = persistentContainer.viewContext 73 | if context.hasChanges { 74 | do { 75 | try context.save() 76 | } catch { 77 | // Replace this implementation with code to handle the error appropriately. 78 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 79 | let nserror = error as NSError 80 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/Entities/CoreDataBook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataBook.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | // It's best to decouple the application / business logic from your persistence framework 32 | // That's why CoreDataBook - which is a NSManagedObjectModel subclass is converted into a Book entity 33 | extension CoreDataBook { 34 | var book: Book { 35 | return Book(id: id ?? "", 36 | isbn: isbn ?? "", 37 | title: title ?? "", 38 | author: author ?? "", 39 | releaseDate: releaseDate as Date?, 40 | pages: Int(pages)) 41 | } 42 | 43 | func populate(with parameters: AddBookParameters) { 44 | // Normally this id should be used at some point during the sync with the API backend 45 | id = NSUUID().uuidString 46 | 47 | isbn = parameters.isbn 48 | title = parameters.title 49 | author = parameters.author 50 | pages = Int32(parameters.pages) 51 | releaseDate = parameters.releaseDate 52 | } 53 | 54 | func populate(with book: Book) { 55 | id = book.id 56 | isbn = book.isbn 57 | title = book.title 58 | author = book.author 59 | pages = Int32(book.pages) 60 | releaseDate = book.releaseDate 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/Gateways/LocalPersistenceBooksGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalPersistenceBooksGateway.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | protocol LocalPersistenceBooksGateway: BooksGateway { 32 | func save(books: [Book]) 33 | 34 | func add(book: Book) 35 | } 36 | 37 | class CoreDataBooksGateway: LocalPersistenceBooksGateway { 38 | let viewContext: NSManagedObjectContextProtocol 39 | 40 | init(viewContext: NSManagedObjectContextProtocol) { 41 | self.viewContext = viewContext 42 | } 43 | 44 | // MARK: - BooksGateway 45 | 46 | func fetchBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 47 | if let coreDataBooks = try? viewContext.allEntities(withType: CoreDataBook.self) { 48 | let books = coreDataBooks.map { $0.book } 49 | completionHandler(.success(books)) 50 | } else { 51 | completionHandler(.failure(CoreError(message: "Failed retrieving books the data base"))) 52 | } 53 | } 54 | 55 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 56 | guard let coreDataBook = viewContext.addEntity(withType: CoreDataBook.self) else { 57 | completionHandler(.failure(CoreError(message: "Failed adding the book in the data base"))) 58 | return 59 | } 60 | 61 | coreDataBook.populate(with: parameters) 62 | 63 | do { 64 | try viewContext.save() 65 | completionHandler(.success(coreDataBook.book)) 66 | } catch { 67 | viewContext.delete(coreDataBook) 68 | completionHandler(.failure(CoreError(message: "Failed saving the context"))) 69 | } 70 | } 71 | 72 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 73 | let predicate = NSPredicate(format: "id==%@", book.id) 74 | 75 | if let coreDataBooks = try? viewContext.allEntities(withType: CoreDataBook.self, predicate: predicate), 76 | let coreDataBook = coreDataBooks.first { 77 | viewContext.delete(coreDataBook) 78 | } else { 79 | completionHandler(.failure(CoreError(message: "Failed retrieving books the data base"))) 80 | return 81 | } 82 | 83 | do { 84 | try viewContext.save() 85 | completionHandler(.success(())) 86 | } catch { 87 | completionHandler(.failure(CoreError(message: "Failed saving the context"))) 88 | } 89 | 90 | } 91 | 92 | // MARK: - LocalPersistenceBooksGateway 93 | 94 | func save(books: [Book]) { 95 | // Save books to core data. Depending on your specific need this might need to be turned into some kind of merge mechanism. 96 | } 97 | 98 | func add(book: Book) { 99 | // Add book. Usually you could call this after the entity was successfully added on the API side or you could use the save method. 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/Library.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Library.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/Library.xcdatamodeld/Library.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Library/EntityGateway/LocalPersistence/NSManagedObjectContext-Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContext-Utils.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | protocol NSManagedObjectContextProtocol { 32 | func allEntities(withType type: T.Type) throws -> [T] 33 | func allEntities(withType type: T.Type, predicate: NSPredicate?) throws -> [T] 34 | func addEntity(withType type : T.Type) -> T? 35 | func save() throws 36 | func delete(_ object: NSManagedObject) 37 | } 38 | 39 | extension NSManagedObjectContext: NSManagedObjectContextProtocol { 40 | func allEntities(withType type: T.Type) throws -> [T] { 41 | return try allEntities(withType: type, predicate: nil) 42 | } 43 | 44 | func allEntities(withType type: T.Type, predicate: NSPredicate?) throws -> [T] { 45 | let request = NSFetchRequest(entityName: T.description()) 46 | request.predicate = predicate 47 | let results = try self.fetch(request) 48 | 49 | return results 50 | } 51 | 52 | func addEntity(withType type: T.Type) -> T? { 53 | let entityName = T.description() 54 | 55 | guard let entity = NSEntityDescription.entity(forEntityName: entityName, in: self) else { 56 | return nil 57 | } 58 | 59 | let record = T(entity: entity, insertInto: self) 60 | 61 | return record 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Library/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIUserInterfaceStyle 45 | Light 46 | 47 | 48 | -------------------------------------------------------------------------------- /Library/Other/Date-RelativeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date-RelativeDescription.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | extension Date { 31 | func relativeDescription(referenceDate: Date = Date()) -> String { 32 | return "Long time ago" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Library/Other/UITextField-EmptyText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField-EmptyText.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | extension UITextField { 31 | var textOrEmptyString: String { 32 | return text ?? "" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Library/Other/UIViewController-Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController-Alert.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/25/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | extension UIViewController { 31 | 32 | func presentAlert(withTitle title:String, message: String) { 33 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 34 | alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) 35 | 36 | present(alert, animated: true, completion: nil) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Library/Other/ViewRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRouter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | protocol ViewRouter { 31 | func prepare(for segue: UIStoryboardSegue, sender: Any?) 32 | } 33 | 34 | extension ViewRouter { 35 | func prepare(for segue: UIStoryboardSegue, sender: Any?) { } 36 | } 37 | -------------------------------------------------------------------------------- /Library/Scenes/AddBook/AddBookConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookConfigurator.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol AddBookConfigurator { 31 | func configure(addBookViewController: AddBookViewController) 32 | } 33 | 34 | class AddBookConfiguratorImplementation: AddBookConfigurator { 35 | var addBookPresenterDelegate: AddBookPresenterDelegate? 36 | 37 | init(addBookPresenterDelegate: AddBookPresenterDelegate?) { 38 | self.addBookPresenterDelegate = addBookPresenterDelegate 39 | } 40 | 41 | func configure(addBookViewController: AddBookViewController) { 42 | let apiClient = ApiClientImplementation(urlSessionConfiguration: URLSessionConfiguration.default, 43 | completionHandlerQueue: OperationQueue.main) 44 | let apiBooksGateway = ApiBooksGatewayImplementation(apiClient: apiClient) 45 | let viewContext = CoreDataStackImplementation.sharedInstance.persistentContainer.viewContext 46 | let coreDataBooksGateway = CoreDataBooksGateway(viewContext: viewContext) 47 | 48 | let booksGateway = CacheBooksGateway(apiBooksGateway: apiBooksGateway, 49 | localPersistenceBooksGateway: coreDataBooksGateway) 50 | 51 | let addBookUseCase = AddBookUseCaseImplementation(booksGateway: booksGateway) 52 | let router = AddBookViewRouterImplementation(addBookViewController: addBookViewController) 53 | 54 | let presenter = AddBookPresenterImplementation(view: addBookViewController, addBookUseCase: addBookUseCase, router: router, delegate: addBookPresenterDelegate) 55 | 56 | addBookViewController.presenter = presenter 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Library/Scenes/AddBook/AddBookPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookPresenter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol AddBookView: class { 31 | func updateAddButtonState(isEnabled enabled: Bool) 32 | func updateCancelButtonState(isEnabled enabled: Bool) 33 | func displayAddBookError(title: String, message: String) 34 | } 35 | 36 | // In the most simple cases (like this one) the delegate wouldn't be needed 37 | // We added it just to highlight how two presenters would communicate 38 | // Most of the time it's fine for the view controller to dimiss itself 39 | protocol AddBookPresenterDelegate: class { 40 | func addBookPresenter(_ presenter: AddBookPresenter, didAdd book: Book) 41 | func addBookPresenterCancel(presenter: AddBookPresenter) 42 | } 43 | 44 | protocol AddBookPresenter { 45 | var router: AddBookViewRouter { get } 46 | var maximumReleaseDate: Date { get } 47 | func addButtonPressed(parameters: AddBookParameters) 48 | func cancelButtonPressed() 49 | } 50 | 51 | class AddBookPresenterImplementation: AddBookPresenter { 52 | fileprivate weak var view: AddBookView? 53 | fileprivate var addBookUseCase: AddBookUseCase 54 | fileprivate weak var delegate: AddBookPresenterDelegate? 55 | private(set) var router: AddBookViewRouter 56 | 57 | var maximumReleaseDate: Date { 58 | return Date() 59 | } 60 | 61 | init(view: AddBookView, 62 | addBookUseCase: AddBookUseCase, 63 | router: AddBookViewRouter, 64 | delegate: AddBookPresenterDelegate?) { 65 | self.view = view 66 | self.addBookUseCase = addBookUseCase 67 | self.router = router 68 | self.delegate = delegate 69 | } 70 | 71 | func addButtonPressed(parameters: AddBookParameters) { 72 | updateNavigationItemsState(isEnabled: false) 73 | addBookUseCase.add(parameters: parameters) { (result) in 74 | self.updateNavigationItemsState(isEnabled: true) 75 | switch result { 76 | case let .success(book): 77 | self.handleBookAdded(book) 78 | case let .failure(error): 79 | self.handleAddBookError(error) 80 | } 81 | } 82 | } 83 | 84 | func cancelButtonPressed() { 85 | delegate?.addBookPresenterCancel(presenter: self) 86 | } 87 | 88 | // MARK: - Private 89 | 90 | fileprivate func handleBookAdded(_ book: Book) { 91 | delegate?.addBookPresenter(self, didAdd: book) 92 | } 93 | 94 | fileprivate func handleAddBookError(_ error: Error) { 95 | // Here we could check the error code and display a localized error message 96 | view?.displayAddBookError(title: "Error", message: error.localizedDescription) 97 | } 98 | 99 | fileprivate func updateNavigationItemsState(isEnabled enabled: Bool) { 100 | view?.updateAddButtonState(isEnabled: enabled) 101 | view?.updateCancelButtonState(isEnabled: enabled) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Library/Scenes/AddBook/AddBookViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookViewController.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | class AddBookViewController: UIViewController, AddBookView { 31 | var presenter: AddBookPresenter! 32 | var configurator: AddBookConfigurator! 33 | 34 | // MARK: - IBOutlets 35 | 36 | @IBOutlet weak var saveButton: UIBarButtonItem! 37 | @IBOutlet weak var cancelButton: UIBarButtonItem! 38 | @IBOutlet weak var isbnTextField: UITextField! 39 | @IBOutlet weak var titleTextField: UITextField! 40 | @IBOutlet weak var authorTextField: UITextField! 41 | @IBOutlet weak var pagesTextField: UITextField! 42 | @IBOutlet weak var releaseDatePicker: UIDatePicker! 43 | 44 | // MARK: - UIViewController 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | configurator.configure(addBookViewController: self) 50 | releaseDatePicker.maximumDate = presenter.maximumReleaseDate 51 | } 52 | 53 | // MARK: - IBActions 54 | 55 | @IBAction func saveButtonPressed(_ sender: Any) { 56 | let addBookParameters = AddBookParameters(isbn: isbnTextField.textOrEmptyString, 57 | title: titleTextField.textOrEmptyString, 58 | author: authorTextField.textOrEmptyString, 59 | releaseDate: releaseDatePicker.date, 60 | pages: Int(pagesTextField.textOrEmptyString) ?? 0) 61 | // Validation on the pages input could be done by a "Validator" class 62 | // Or by the presenter - in which case we should pass it the actual string 63 | presenter.addButtonPressed(parameters: addBookParameters) 64 | } 65 | 66 | @IBAction func cancelButtonPressed(_ sender: Any) { 67 | presenter.cancelButtonPressed() 68 | } 69 | 70 | // MARK: - AddBookView 71 | 72 | func updateCancelButtonState(isEnabled enabled: Bool) { 73 | cancelButton.isEnabled = enabled 74 | } 75 | 76 | func updateAddButtonState(isEnabled enabled: Bool) { 77 | saveButton.isEnabled = enabled 78 | } 79 | 80 | func displayAddBookError(title:String, message: String) { 81 | presentAlert(withTitle: title, message: message) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Library/Scenes/AddBook/AddBookViewRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookViewRouter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol AddBookViewRouter: ViewRouter { 31 | func dismiss() 32 | } 33 | 34 | class AddBookViewRouterImplementation: AddBookViewRouter { 35 | fileprivate weak var addBookViewController: AddBookViewController? 36 | 37 | init(addBookViewController: AddBookViewController) { 38 | self.addBookViewController = addBookViewController 39 | } 40 | 41 | // MARK: - AddBookRouter 42 | 43 | func dismiss() { 44 | addBookViewController?.dismiss(animated: true, completion: nil) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Library/Scenes/BookDetails/BookDetailsConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookDetailsConfigurator.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol BookDetailsConfigurator { 31 | func configure(bookDetailsTableViewController: BookDetailsTableViewController) 32 | } 33 | 34 | class BookDetailsConfiguratorImplementation: BookDetailsConfigurator { 35 | 36 | let book: Book 37 | 38 | init(book: Book) { 39 | self.book = book 40 | } 41 | 42 | func configure(bookDetailsTableViewController: BookDetailsTableViewController) { 43 | let apiClient = ApiClientImplementation(urlSessionConfiguration: URLSessionConfiguration.default, 44 | completionHandlerQueue: OperationQueue.main) 45 | let apiBooksGateway = ApiBooksGatewayImplementation(apiClient: apiClient) 46 | let viewContext = CoreDataStackImplementation.sharedInstance.persistentContainer.viewContext 47 | let coreDataBooksGateway = CoreDataBooksGateway(viewContext: viewContext) 48 | 49 | let booksGateway = CacheBooksGateway(apiBooksGateway: apiBooksGateway, 50 | localPersistenceBooksGateway: coreDataBooksGateway) 51 | 52 | let deleteProgrammeUseCase = DeleteBookUseCaseImplementation(booksGateway: booksGateway) 53 | let router = BookDetailsViewRouterImplementation(bookDetailsTableViewController: bookDetailsTableViewController) 54 | 55 | let presenter = BookDetailsPresenterImplementation(view: bookDetailsTableViewController, 56 | book: book, 57 | deleteBookUseCase: deleteProgrammeUseCase, 58 | router: router) 59 | 60 | 61 | bookDetailsTableViewController.presenter = presenter 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Library/Scenes/BookDetails/BookDetailsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookDetailsPresenter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol BookDetailsView: class { 31 | func displayScreenTitle(title: String) 32 | func display(id: String) 33 | func display(isbn: String) 34 | func display(title: String) 35 | func display(author: String) 36 | func display(pages: String) 37 | func display(releaseDate: String) 38 | func displayDeleteBookError(title: String, message: String) 39 | func updateCancelButtonState(isEnabled enabled: Bool) 40 | } 41 | 42 | protocol BookDetailsPresenter { 43 | var router: BookDetailsViewRouter { get } 44 | func viewDidLoad() 45 | func deleteButtonPressed() 46 | } 47 | 48 | class BookDetailsPresenterImplementation: BookDetailsPresenter { 49 | fileprivate let book: Book 50 | fileprivate let deleteBookUseCase: DeleteBookUseCase 51 | let router: BookDetailsViewRouter 52 | fileprivate weak var view: BookDetailsView? 53 | 54 | init(view: BookDetailsView, 55 | book: Book, 56 | deleteBookUseCase: DeleteBookUseCase, 57 | router: BookDetailsViewRouter) { 58 | self.view = view 59 | self.book = book 60 | self.deleteBookUseCase = deleteBookUseCase 61 | self.router = router 62 | } 63 | 64 | func viewDidLoad() { 65 | view?.display(id: book.id) 66 | view?.display(isbn: book.isbn) 67 | view?.display(title: book.title) 68 | view?.display(author: book.author) 69 | view?.display(pages: "\(book.pages) pages") 70 | view?.display(releaseDate: book.releaseDate?.relativeDescription() ?? "") 71 | } 72 | 73 | func deleteButtonPressed() { 74 | view?.updateCancelButtonState(isEnabled: false) 75 | deleteBookUseCase.delete(book: book) { (result) in 76 | self.view?.updateCancelButtonState(isEnabled: true) 77 | switch result { 78 | case .success(_): 79 | self.handleBookDeleted() 80 | case let .failure(error): 81 | self.handleBookDeleteError(error) 82 | } 83 | } 84 | } 85 | 86 | // MARK: - Private 87 | 88 | fileprivate func handleBookDeleted() { 89 | // Here we could use a similar approach like on AddBookViewController and call a delegate like we do when adding a book 90 | // However we want to provide a different example - depending on the complexity of you particular case 91 | // You can chose one way or the other 92 | router.dismissView() 93 | } 94 | 95 | fileprivate func handleBookDeleteError(_ error: Error) { 96 | // Here we could check the error code and display a localized error message 97 | view?.displayDeleteBookError(title: "Error", message: error.localizedDescription) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Library/Scenes/BookDetails/BookDetailsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookDetailsTableViewController.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | class BookDetailsTableViewController: UITableViewController, BookDetailsView { 31 | var presenter: BookDetailsPresenter! 32 | var configurator: BookDetailsConfigurator! 33 | 34 | // MARK: - IBOutlets 35 | @IBOutlet weak var idLabel: UILabel! 36 | @IBOutlet weak var isbnLabel: UILabel! 37 | @IBOutlet weak var titleLabel: UILabel! 38 | @IBOutlet weak var authorLabel: UILabel! 39 | @IBOutlet weak var pagesLabel: UILabel! 40 | @IBOutlet weak var releaseDateLabel: UILabel! 41 | @IBOutlet weak var deleteButton: UIBarButtonItem! 42 | 43 | // MARK: - UIViewController 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | configurator.configure(bookDetailsTableViewController: self) 49 | presenter.viewDidLoad() 50 | } 51 | 52 | // MARK: IBAction 53 | 54 | @IBAction func deleteButtonPressed(_ sender: Any) { 55 | presenter.deleteButtonPressed() 56 | } 57 | 58 | // MARK: - BookDetailsView 59 | 60 | func displayScreenTitle(title: String) { 61 | self.title = title 62 | } 63 | 64 | func display(id: String) { 65 | idLabel.text = id 66 | } 67 | 68 | func display(isbn: String) { 69 | isbnLabel.text = isbn 70 | } 71 | 72 | func display(title: String) { 73 | titleLabel.text = title 74 | } 75 | 76 | func display(author: String) { 77 | authorLabel.text = author 78 | } 79 | 80 | func display(pages: String) { 81 | pagesLabel.text = pages 82 | } 83 | 84 | func display(releaseDate: String) { 85 | releaseDateLabel.text = releaseDate 86 | } 87 | 88 | func displayDeleteBookError(title: String, message: String) { 89 | presentAlert(withTitle: title, message: message) 90 | } 91 | 92 | func updateCancelButtonState(isEnabled enabled: Bool) { 93 | deleteButton.isEnabled = enabled 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Library/Scenes/BookDetails/BookDetailsViewRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookDetailsViewRouter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol BookDetailsViewRouter: ViewRouter { 31 | func dismissView() 32 | } 33 | 34 | class BookDetailsViewRouterImplementation: BookDetailsViewRouter { 35 | fileprivate weak var bookDetailsTableViewController: BookDetailsTableViewController? 36 | 37 | init(bookDetailsTableViewController: BookDetailsTableViewController) { 38 | self.bookDetailsTableViewController = bookDetailsTableViewController 39 | } 40 | 41 | func dismissView() { 42 | let _ = bookDetailsTableViewController?.navigationController?.popViewController(animated: true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Library/Scenes/Books/BookTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookTableViewCell.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | class BookTableViewCell: UITableViewCell, BookCellView { 31 | 32 | @IBOutlet weak var bookTitleLabel: UILabel! 33 | @IBOutlet weak var bookAuthorLabel: UILabel! 34 | @IBOutlet weak var bookReleaseDateLabel: UILabel! 35 | 36 | 37 | func display(title: String) { 38 | bookTitleLabel.text = title 39 | } 40 | 41 | func display(author: String) { 42 | bookAuthorLabel.text = author 43 | } 44 | 45 | func display(releaseDate: String) { 46 | bookReleaseDateLabel.text = releaseDate 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Library/Scenes/Books/BooksConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksConfigurator.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol BooksConfigurator { 31 | func configure(booksTableViewController: BooksTableViewController) 32 | } 33 | 34 | class BooksConfiguratorImplementation: BooksConfigurator { 35 | 36 | func configure(booksTableViewController: BooksTableViewController) { 37 | let apiClient = ApiClientImplementation(urlSessionConfiguration: URLSessionConfiguration.default, 38 | completionHandlerQueue: OperationQueue.main) 39 | let apiBooksGateway = ApiBooksGatewayImplementation(apiClient: apiClient) 40 | let viewContext = CoreDataStackImplementation.sharedInstance.persistentContainer.viewContext 41 | let coreDataBooksGateway = CoreDataBooksGateway(viewContext: viewContext) 42 | 43 | let booksGateway = CacheBooksGateway(apiBooksGateway: apiBooksGateway, 44 | localPersistenceBooksGateway: coreDataBooksGateway) 45 | 46 | let displayBooksUseCase = DisplayBooksListUseCaseImplementation(booksGateway: booksGateway) 47 | let deleteBookUseCase = DeleteBookUseCaseImplementation(booksGateway: booksGateway) 48 | let router = BooksViewRouterImplementation(booksTableViewController: booksTableViewController) 49 | 50 | let presenter = BooksPresenterImplementation(view: booksTableViewController, 51 | displayBooksUseCase: displayBooksUseCase, 52 | deleteBookUseCase: deleteBookUseCase, 53 | router: router) 54 | 55 | booksTableViewController.presenter = presenter 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Library/Scenes/Books/BooksPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksPresenter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | protocol BooksView: class { 31 | func refreshBooksView() 32 | func displayBooksRetrievalError(title: String, message: String) 33 | func displayBookDeleteError(title: String, message: String) 34 | func deleteAnimated(row: Int) 35 | func endEditing() 36 | } 37 | 38 | // It would be fine for the cell view to declare a BookCellViewModel property and have it configure itself 39 | // Using this approach makes the view even more passive/dumb - but I can understand if some might consider it an overkill 40 | protocol BookCellView { 41 | func display(title: String) 42 | func display(author: String) 43 | func display(releaseDate: String) 44 | } 45 | 46 | protocol BooksPresenter { 47 | var numberOfBooks: Int { get } 48 | var router: BooksViewRouter { get } 49 | func viewDidLoad() 50 | func configure(cell: BookCellView, forRow row: Int) 51 | func didSelect(row: Int) 52 | func canEdit(row: Int) -> Bool 53 | func titleForDeleteButton(row: Int) -> String 54 | func deleteButtonPressed(row: Int) 55 | func addButtonPressed() 56 | } 57 | 58 | class BooksPresenterImplementation: BooksPresenter, AddBookPresenterDelegate { 59 | fileprivate weak var view: BooksView? 60 | fileprivate let displayBooksUseCase: DisplayBooksUseCase 61 | fileprivate let deleteBookUseCase: DeleteBookUseCase 62 | internal let router: BooksViewRouter 63 | 64 | // Normally this would be file private as well, we keep it internal so we can inject values for testing purposes 65 | var books = [Book]() 66 | 67 | var numberOfBooks: Int { 68 | return books.count 69 | } 70 | 71 | init(view: BooksView, 72 | displayBooksUseCase: DisplayBooksUseCase, 73 | deleteBookUseCase: DeleteBookUseCase, 74 | router: BooksViewRouter) { 75 | self.view = view 76 | self.displayBooksUseCase = displayBooksUseCase 77 | self.deleteBookUseCase = deleteBookUseCase 78 | self.router = router 79 | 80 | registerForDeleteBookNotification() 81 | } 82 | 83 | deinit { 84 | NotificationCenter.default.removeObserver(self) 85 | } 86 | 87 | // MARK: - BooksPresenter 88 | 89 | func viewDidLoad() { 90 | self.displayBooksUseCase.displayBooks { (result) in 91 | switch result { 92 | case let .success(books): 93 | self.handleBooksReceived(books) 94 | case let .failure(error): 95 | self.handleBooksError(error) 96 | } 97 | } 98 | } 99 | 100 | func configure(cell: BookCellView, forRow row: Int) { 101 | let book = books[row] 102 | 103 | cell.display(title: book.title) 104 | cell.display(author: book.author) 105 | cell.display(releaseDate: book.releaseDate?.relativeDescription() ?? "Unknown") 106 | } 107 | 108 | func didSelect(row: Int) { 109 | let book = books[row] 110 | 111 | router.presentDetailsView(for: book) 112 | } 113 | 114 | func canEdit(row: Int) -> Bool { 115 | return true 116 | } 117 | 118 | func titleForDeleteButton(row: Int) -> String { 119 | return "Delete Book" 120 | } 121 | 122 | func deleteButtonPressed(row: Int) { 123 | view?.endEditing() 124 | 125 | let book = books[row] 126 | deleteBookUseCase.delete(book: book) { (result) in 127 | switch result { 128 | case .success(): 129 | self.handleBookDeleted(book: book) 130 | case let .failure(error): 131 | self.handleBookDeleteError(error) 132 | } 133 | } 134 | } 135 | 136 | func addButtonPressed() { 137 | router.presentAddBook(addBookPresenterDelegate: self) 138 | } 139 | 140 | // MARK: - AddBookPresenterDelegate 141 | 142 | func addBookPresenter(_ presenter: AddBookPresenter, didAdd book: Book) { 143 | presenter.router.dismiss() 144 | books.append(book) 145 | view?.refreshBooksView() 146 | } 147 | 148 | func addBookPresenterCancel(presenter: AddBookPresenter) { 149 | presenter.router.dismiss() 150 | } 151 | 152 | // MARK: - Private 153 | 154 | fileprivate func handleBooksReceived(_ books: [Book]) { 155 | self.books = books 156 | view?.refreshBooksView() 157 | } 158 | 159 | fileprivate func handleBooksError(_ error: Error) { 160 | // Here we could check the error code and display a localized error message 161 | view?.displayBooksRetrievalError(title: "Error", message: error.localizedDescription) 162 | } 163 | 164 | fileprivate func registerForDeleteBookNotification() { 165 | NotificationCenter.default.addObserver(self, 166 | selector: #selector(didReceiveDeleteBookNotification), 167 | name: DeleteBookUseCaseNotifications.didDeleteBook, 168 | object: nil) 169 | } 170 | 171 | @objc fileprivate func didReceiveDeleteBookNotification(_ notification: Notification) { 172 | if let book = notification.object as? Book { 173 | handleBookDeleted(book: book) 174 | } 175 | } 176 | 177 | fileprivate func handleBookDeleted(book: Book) { 178 | // The book might have already be deleted due to the notification response 179 | if let row = books.firstIndex(of: book) { 180 | books.remove(at: row) 181 | view?.deleteAnimated(row: row) 182 | } 183 | } 184 | 185 | fileprivate func handleBookDeleteError(_ error: Error) { 186 | // Here we could check the error code and display a localized error message 187 | view?.displayBookDeleteError(title: "Error", message: error.localizedDescription) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Library/Scenes/Books/BooksTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksTableViewController.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | class BooksTableViewController: UITableViewController, BooksView { 31 | var configurator = BooksConfiguratorImplementation() 32 | var presenter: BooksPresenter! 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | configurator.configure(booksTableViewController: self) 38 | presenter.viewDidLoad() 39 | } 40 | 41 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 42 | presenter.router.prepare(for: segue, sender: sender) 43 | } 44 | 45 | // MARK: - IBAction 46 | 47 | @IBAction func addButtonPressed(_ sender: Any) { 48 | presenter.addButtonPressed() 49 | } 50 | 51 | // MARK: - UITableViewDataSource 52 | 53 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return presenter.numberOfBooks 55 | } 56 | 57 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: "BookTableViewCell", for: indexPath) as! BookTableViewCell 59 | presenter.configure(cell: cell, forRow: indexPath.row) 60 | 61 | return cell 62 | } 63 | 64 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 65 | presenter.didSelect(row: indexPath.row) 66 | } 67 | 68 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 69 | return presenter.canEdit(row: indexPath.row) 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, 73 | titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { 74 | return presenter.titleForDeleteButton(row: indexPath.row) 75 | } 76 | 77 | override func tableView(_ tableView: UITableView, 78 | commit editingStyle: UITableViewCell.EditingStyle, 79 | forRowAt indexPath: IndexPath) { 80 | presenter.deleteButtonPressed(row: indexPath.row) 81 | } 82 | 83 | // MARK: - BooksView 84 | 85 | func refreshBooksView() { 86 | tableView.reloadData() 87 | } 88 | 89 | func displayBooksRetrievalError(title: String, message: String) { 90 | presentAlert(withTitle: title, message: message) 91 | } 92 | 93 | func deleteAnimated(row: Int) { 94 | tableView.deleteRows(at: [IndexPath(row: row, section:0)], with: .automatic) 95 | } 96 | 97 | func endEditing() { 98 | tableView.setEditing(false, animated: true) 99 | } 100 | 101 | func displayBookDeleteError(title: String, message: String) { 102 | presentAlert(withTitle: title, message: message) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Library/Scenes/Books/BooksViewRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksViewRouter.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/23/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import UIKit 29 | 30 | protocol BooksViewRouter: ViewRouter { 31 | func presentDetailsView(for book: Book) 32 | func presentAddBook(addBookPresenterDelegate: AddBookPresenterDelegate) 33 | } 34 | 35 | class BooksViewRouterImplementation: BooksViewRouter { 36 | fileprivate weak var booksTableViewController: BooksTableViewController? 37 | fileprivate weak var addBookPresenterDelegate: AddBookPresenterDelegate? 38 | fileprivate var book: Book! 39 | 40 | init(booksTableViewController: BooksTableViewController) { 41 | self.booksTableViewController = booksTableViewController 42 | } 43 | 44 | // MARK: - BooksViewRouter 45 | 46 | func presentDetailsView(for book: Book) { 47 | self.book = book 48 | booksTableViewController?.performSegue(withIdentifier: "BooksSceneToBookDetailsSceneSegue", sender: nil) 49 | } 50 | 51 | func presentAddBook(addBookPresenterDelegate: AddBookPresenterDelegate) { 52 | self.addBookPresenterDelegate = addBookPresenterDelegate 53 | booksTableViewController?.performSegue(withIdentifier: "BooksSceneToAddBookSceneSegue", sender: nil) 54 | } 55 | 56 | func prepare(for segue: UIStoryboardSegue, sender: Any?) { 57 | if let bookDetailsTableViewController = segue.destination as? BookDetailsTableViewController { 58 | bookDetailsTableViewController.configurator = BookDetailsConfiguratorImplementation(book: book) 59 | } else if let navigationController = segue.destination as? UINavigationController, 60 | let addBookViewController = navigationController.topViewController as? AddBookViewController { 61 | addBookViewController.configurator = AddBookConfiguratorImplementation(addBookPresenterDelegate: addBookPresenterDelegate) 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /LibraryTests/Gateways/ApiClientTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiClientTest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/2/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import XCTest 29 | @testable import Library 30 | 31 | class ApiClientTest: XCTestCase { 32 | // https://www.martinfowler.com/bliki/TestDouble.html 33 | let urlSessionStub = URLSessionStub() 34 | 35 | var apiClient: ApiClientImplementation! 36 | 37 | 38 | // MARK: - Set up 39 | 40 | override func setUp() { 41 | super.setUp() 42 | apiClient = ApiClientImplementation(urlSession: urlSessionStub) 43 | } 44 | 45 | // MARK: - Tests 46 | 47 | func test_execute_successful_http_response_parses_ok() { 48 | // Given 49 | 50 | // Normally to mock JSON responses you should use a Dictionary and convert it to JSON using JSONSerialization.data 51 | // In our example here we don't care about the actual JSON, we care about the data regardless of its format it would have 52 | let expectedUtf8StringResponse = "{\"SomeProperty\":\"SomeValue\"}" 53 | let expectedData = expectedUtf8StringResponse.data(using: .utf8) 54 | let expected2xxReponse = HTTPURLResponse(statusCode: 200) 55 | 56 | urlSessionStub.enqueue(response: (data: expectedData, response: expected2xxReponse, error: nil)) 57 | 58 | let executeCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 59 | 60 | // When 61 | apiClient.execute(request: TestDoubleRequest()) { (result: Result>) in 62 | // Then 63 | guard let response = try? result.get() else { 64 | XCTFail("A successfull response should've been returned") 65 | return 66 | } 67 | XCTAssertEqual(expectedUtf8StringResponse, response.entity.utf8String, "The string is not the expected one") 68 | XCTAssertTrue(expected2xxReponse === response.httpUrlResponse, "The http response is not the expected one") 69 | XCTAssertEqual(expectedData?.base64EncodedString(), response.data?.base64EncodedString(), "Data doesn't match") 70 | 71 | executeCompletionHandlerExpectation.fulfill() 72 | } 73 | 74 | waitForExpectations(timeout: 1, handler: nil) 75 | } 76 | 77 | func test_execute_successful_http_response_prase_error() { 78 | // Given 79 | let expectedUtf8StringResponse = "{ \"SomeProperty\" : \"SomeValue\" }" 80 | let expectedData = expectedUtf8StringResponse.data(using: .utf8) 81 | let expected2xxReponse = HTTPURLResponse(statusCode: 200) 82 | let expectedParsingErrorMessage = "A parsing error occured" 83 | 84 | urlSessionStub.enqueue(response: (data: expectedData, response: expected2xxReponse, error: nil)) 85 | 86 | let executeCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 87 | 88 | // When 89 | apiClient.execute(request: TestDoubleRequest()) { (result: Result>) in 90 | // Then 91 | do { 92 | let _ = try result.get() 93 | XCTFail("Expected parse error to be thrown") 94 | } catch let error as ApiParseError { 95 | XCTAssertTrue(expected2xxReponse === error.httpUrlResponse, "The http response is not the expected one") 96 | XCTAssertEqual(expectedData?.base64EncodedString(), error.data?.base64EncodedString(), "Data doesn't match") 97 | XCTAssertEqual(expectedParsingErrorMessage, error.localizedDescription, "Error message doesn't match") 98 | } catch { 99 | XCTFail("Expected parse error to be thrown") 100 | } 101 | 102 | executeCompletionHandlerExpectation.fulfill() 103 | } 104 | 105 | waitForExpectations(timeout: 1, handler: nil) 106 | } 107 | 108 | func test_execute_non_2xx_response_code() { 109 | let expectedUtf8StringResponse = "{ \"SomeProperty\" : \"SomeValue\" }" 110 | let expectedData = expectedUtf8StringResponse.data(using: .utf8) 111 | let expected4xxReponse = HTTPURLResponse(statusCode: 400) 112 | 113 | urlSessionStub.enqueue(response: (data: expectedData, response: expected4xxReponse, error: nil)) 114 | 115 | let executeCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 116 | 117 | // When 118 | apiClient.execute(request: TestDoubleRequest()) { (result: Result>) in 119 | // Then 120 | do { 121 | let _ = try result.get() 122 | XCTFail("Expected api error to be thrown") 123 | } catch let error as ApiError { 124 | XCTAssertTrue(expected4xxReponse === error.httpUrlResponse, "The http response is not the expected one") 125 | XCTAssertEqual(expectedData?.base64EncodedString(), error.data?.base64EncodedString(), "Data doesn't match") 126 | } catch { 127 | XCTFail("Expected api error to be thrown") 128 | } 129 | 130 | executeCompletionHandlerExpectation.fulfill() 131 | } 132 | 133 | waitForExpectations(timeout: 1, handler: nil) 134 | } 135 | 136 | func test_execute_error_no_httpurlresponse() { 137 | // Given 138 | let expectedErrorMessage = "Some random network error" 139 | let expectedError = NSError.createError(withMessage: expectedErrorMessage) 140 | 141 | urlSessionStub.enqueue(response: (data: nil, response: nil, error: expectedError)) 142 | 143 | let executeCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 144 | 145 | // When 146 | apiClient.execute(request: TestDoubleRequest()) { (result: Result>) in 147 | // Then 148 | do { 149 | let _ = try result.get() 150 | XCTFail("Expected network error to be thrown") 151 | } catch let error as NetworkRequestError { 152 | XCTAssertEqual(expectedErrorMessage, error.localizedDescription, "Error message doesn't match") 153 | } catch { 154 | XCTFail("Expected network error to be thrown") 155 | } 156 | 157 | executeCompletionHandlerExpectation.fulfill() 158 | } 159 | 160 | waitForExpectations(timeout: 1, handler: nil) 161 | } 162 | } 163 | 164 | private struct TestDoubleRequest: ApiRequest { 165 | var urlRequest: URLRequest { 166 | return URLRequest(url: URL.googleUrl) 167 | } 168 | } 169 | 170 | private struct TestDoubleApiEntity: Codable { 171 | var SomeProperty: String 172 | 173 | var utf8String: String { 174 | let data = try? JSONEncoder().encode(self) 175 | return String(data: data!, encoding: .utf8)! 176 | } 177 | } 178 | 179 | private struct TestDoubleErrorParseApiEntity: Decodable { 180 | init(from decoder: Decoder) throws { 181 | throw NSError.createPraseError() 182 | } 183 | } 184 | 185 | 186 | -------------------------------------------------------------------------------- /LibraryTests/Gateways/CacheBooksGatewayTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheBooksGatewayTest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | import XCTest 28 | 29 | @testable import Library 30 | 31 | class CacheBooksGatewayTest: XCTestCase { 32 | // https://www.martinfowler.com/bliki/TestDouble.html 33 | var apiBooksGatewaySpy = ApiBooksGatewaySpy() 34 | var localPersistenceBooksGatewaySpy = LocalPersistenceBooksGatewaySpy() 35 | 36 | var cacheBooksGateway: CacheBooksGateway! 37 | 38 | // MARK: - Set up 39 | 40 | override func setUp() { 41 | super.setUp() 42 | cacheBooksGateway = CacheBooksGateway(apiBooksGateway: apiBooksGatewaySpy, 43 | localPersistenceBooksGateway: localPersistenceBooksGatewaySpy) 44 | } 45 | 46 | // MARK: - Tests 47 | 48 | func test_fetchBooks_api_success_save_locally() { 49 | // Given 50 | let booksToReturn = Book.createBooksArray() 51 | let expectedResultToBeReturned: Result<[Book]> = .success(booksToReturn) 52 | 53 | apiBooksGatewaySpy.fetchBooksResultToBeReturned = expectedResultToBeReturned 54 | 55 | let fetchBooksCompletionHandlerExpectation = expectation(description: "Fetch books completion handler expectation") 56 | 57 | // When 58 | cacheBooksGateway.fetchBooks { (result) in 59 | // Then 60 | XCTAssertTrue(expectedResultToBeReturned == result, "The expected result wasn't returned") 61 | XCTAssertEqual(booksToReturn, self.localPersistenceBooksGatewaySpy.booksSaved, "The books weren't saved on the local persistence") 62 | 63 | fetchBooksCompletionHandlerExpectation.fulfill() 64 | } 65 | 66 | waitForExpectations(timeout: 1, handler: nil) 67 | } 68 | 69 | func test_fetchBooks_api_failure_fetch_from_local_persistence() { 70 | // Given 71 | let booksToReturnFromLocalPersistence = Book.createBooksArray() 72 | let expectedResultToBeReturnedFromLocalPersistence: Result<[Book]> = .success(booksToReturnFromLocalPersistence) 73 | let expectedResultFromApi: Result<[Book]> = .failure(NSError.createError(withMessage: "Some error fetching books")) 74 | 75 | apiBooksGatewaySpy.fetchBooksResultToBeReturned = expectedResultFromApi 76 | localPersistenceBooksGatewaySpy.fetchBooksResultToBeReturned = expectedResultToBeReturnedFromLocalPersistence 77 | 78 | let fetchBooksCompletionHandlerExpectation = expectation(description: "Fetch books completion handler expectation") 79 | 80 | // When 81 | cacheBooksGateway.fetchBooks { (result) in 82 | // Then 83 | XCTAssertTrue(expectedResultToBeReturnedFromLocalPersistence == result, "The expected result wasn't returned") 84 | XCTAssertTrue(self.localPersistenceBooksGatewaySpy.fetchBooksCalled, "Fetch books wasn't called on the local persistence") 85 | 86 | fetchBooksCompletionHandlerExpectation.fulfill() 87 | } 88 | 89 | waitForExpectations(timeout: 1, handler: nil) 90 | } 91 | 92 | func test_add_api_success_add_locally_as_well() { 93 | // Given 94 | let bookToAdd = Book.createBook() 95 | let expectedResultToBeReturned: Result = .success(bookToAdd) 96 | let addBookParameters = AddBookParameters.createParameters() 97 | apiBooksGatewaySpy.addBookResultToBeReturned = expectedResultToBeReturned 98 | 99 | let addBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 100 | 101 | // When 102 | cacheBooksGateway.add(parameters: addBookParameters) { (result) in 103 | // Then 104 | XCTAssertTrue(expectedResultToBeReturned == result, "The expected result wasn't returned") 105 | XCTAssertEqual(addBookParameters, self.apiBooksGatewaySpy.addBookParameters, "Add book parameters passed to API mismatch") 106 | XCTAssertEqual(bookToAdd, self.localPersistenceBooksGatewaySpy.addedBook, "The added book wasn't passed to the local persistence") 107 | 108 | addBookCompletionHandlerExpectation.fulfill() 109 | } 110 | 111 | waitForExpectations(timeout: 1, handler: nil) 112 | } 113 | 114 | func test_add_api_failure_add_locally() { 115 | // Given 116 | let bookToAdd = Book.createBook() 117 | let expectedResultToBeReturnedFromLocalPersistence: Result = .success(bookToAdd) 118 | let expectedResultsFromApi: Result! = .failure(NSError.createError(withMessage: "Some error adding book")) 119 | let addBookParameters = AddBookParameters.createParameters() 120 | 121 | apiBooksGatewaySpy.addBookResultToBeReturned = expectedResultsFromApi 122 | localPersistenceBooksGatewaySpy.addBookResultToBeReturned = expectedResultToBeReturnedFromLocalPersistence 123 | 124 | let addBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 125 | 126 | // When 127 | cacheBooksGateway.add(parameters: addBookParameters) { (result) in 128 | // Then 129 | XCTAssertTrue(expectedResultToBeReturnedFromLocalPersistence == result, "The expected result wasn't returned") 130 | XCTAssertEqual(addBookParameters, self.apiBooksGatewaySpy.addBookParameters, "Add book parameters passed to API mismatch") 131 | XCTAssertEqual(addBookParameters, self.localPersistenceBooksGatewaySpy.addBookParameters, "Add book parameters passed to local persistence mismatch") 132 | 133 | addBookCompletionHandlerExpectation.fulfill() 134 | } 135 | 136 | waitForExpectations(timeout: 1, handler: nil) 137 | } 138 | 139 | func test_delete_api_delete_return_response_from_local_repo() { 140 | // Given 141 | let bookToDelete = Book.createBook() 142 | let expectedResultToBeReturnedFromApi: Result = .failure(NSError.createError(withMessage: "Some error delete book")) 143 | let expectedResultToBeReturnedFromLocalPersistence: Result = .success(()) 144 | apiBooksGatewaySpy.deleteBookResultToBeReturned = expectedResultToBeReturnedFromApi 145 | localPersistenceBooksGatewaySpy.deleteBookResultToBeReturned = expectedResultToBeReturnedFromLocalPersistence 146 | 147 | let deleteBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 148 | 149 | // When 150 | cacheBooksGateway.delete(book: bookToDelete) { (result) in 151 | XCTAssertTrue(expectedResultToBeReturnedFromLocalPersistence == result, "The expected result wasn't returned") 152 | XCTAssertEqual(bookToDelete, self.apiBooksGatewaySpy.deletedBook, "Book to delete wasn't passed to the API") 153 | XCTAssertEqual(bookToDelete, self.localPersistenceBooksGatewaySpy.deletedBook, "Book to delete wasn't passed to the local persistence") 154 | deleteBookCompletionHandlerExpectation.fulfill() 155 | } 156 | 157 | waitForExpectations(timeout: 1, handler: nil) 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /LibraryTests/Gateways/CoreDataBooksGatewayTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataBooksGatewayTest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/1/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import XCTest 29 | 30 | @testable import Library 31 | 32 | // Discussion: 33 | // Happy path is tested using an in memory core data stack while the error paths are "simulated" using a stub NSManagedObjectContextStub 34 | // Probably you could use NSManagedObjectContextStub for happy path testing as well, however you might not be able to instantiate a NSManagedObject subclass 35 | // without a valid context 36 | class CoreDataBooksGatewayTest: XCTestCase { 37 | // https://www.martinfowler.com/bliki/TestDouble.html 38 | var inMemoryCoreDataStack = InMemoryCoreDataStack() 39 | var managedObjectContextSpy = NSManagedObjectContextSpy() 40 | 41 | var inMemoryCoreDataBooksGateway: CoreDataBooksGateway { 42 | return CoreDataBooksGateway(viewContext: inMemoryCoreDataStack.persistentContainer.viewContext) 43 | } 44 | 45 | var errorPathCoreDataBooksGateway: CoreDataBooksGateway { 46 | return CoreDataBooksGateway(viewContext: managedObjectContextSpy) 47 | } 48 | 49 | // MARK: - Tests 50 | 51 | // Normally add and fetchBooks should be tested independently, but in this case since we're not mocking the 52 | // Core Data piece (we're using an in-memory persistent store) it's fine to test the one via the other 53 | func test_add_with_parameters_fetchBooks_withParameters_success() { 54 | // Given 55 | let addBookParameters = AddBookParameters.createParameters() 56 | 57 | let addBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 58 | let fetchBooksCompletionHandlerExpectation = expectation(description: "Fetch books completion handler expectation") 59 | 60 | // When 61 | inMemoryCoreDataBooksGateway.add(parameters: addBookParameters) { (result) in 62 | // Then 63 | guard let book = try? result.get() else { 64 | XCTFail("Should've saved the book with success") 65 | return 66 | } 67 | 68 | Assert(book: book, builtFromParameters: addBookParameters) 69 | Assert(book: book, wasAddedIn: self.inMemoryCoreDataBooksGateway, expectation: fetchBooksCompletionHandlerExpectation) 70 | 71 | addBookCompletionHandlerExpectation.fulfill() 72 | } 73 | 74 | waitForExpectations(timeout: 1, handler: nil) 75 | } 76 | 77 | func test_fetch_failure() { 78 | // Given 79 | let expectedResultToBeReturned: Result<[Book]> = .failure(CoreError(message: "Failed retrieving books the data base")) 80 | managedObjectContextSpy.fetchErrorToThrow = NSError.createError(withMessage: "Some core data error") 81 | 82 | let fetchBooksCompletionHandlerExpectation = expectation(description: "Fetch books completion handler expectation") 83 | 84 | // When 85 | errorPathCoreDataBooksGateway.fetchBooks { (result) in 86 | // Then 87 | XCTAssertTrue(expectedResultToBeReturned == result, "Failure error wasn't returned") 88 | fetchBooksCompletionHandlerExpectation.fulfill() 89 | } 90 | 91 | waitForExpectations(timeout: 1, handler: nil) 92 | } 93 | 94 | func test_add_with_parameters_fails_without_reaching_save() { 95 | // Given 96 | let expectedResultToBeReturned: Result = .failure(CoreError(message: "Failed adding the book in the data base")) 97 | managedObjectContextSpy.addEntityToReturn = nil 98 | 99 | let addBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 100 | 101 | // When 102 | errorPathCoreDataBooksGateway.add(parameters: AddBookParameters.createParameters()) { (result) in 103 | // Then 104 | XCTAssertTrue(expectedResultToBeReturned == result, "Failure error wasn't returned") 105 | addBookCompletionHandlerExpectation.fulfill() 106 | } 107 | 108 | waitForExpectations(timeout: 1, handler: nil) 109 | } 110 | 111 | func test_add_with_parameters_fails_when_saving() { 112 | // Given 113 | let expectedResultToBeReturned: Result = .failure(CoreError(message: "Failed saving the context")) 114 | let addedCoreDataBook = inMemoryCoreDataStack.fakeEntity(withType: CoreDataBook.self) 115 | managedObjectContextSpy.addEntityToReturn = addedCoreDataBook 116 | managedObjectContextSpy.saveErrorToReturn = NSError.createError(withMessage: "Some core data error") 117 | 118 | let addBookCompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 119 | 120 | // When 121 | errorPathCoreDataBooksGateway.add(parameters: AddBookParameters.createParameters()) { (result) in 122 | // Then 123 | XCTAssertTrue(expectedResultToBeReturned == result, "Failure error wasn't returned") 124 | XCTAssertTrue(self.managedObjectContextSpy.deletedObject! === addedCoreDataBook, "The inserted entity should've been deleted") 125 | addBookCompletionHandlerExpectation.fulfill() 126 | } 127 | 128 | waitForExpectations(timeout: 1, handler: nil) 129 | } 130 | 131 | func test_deleteBook_success() { 132 | // Given 133 | let addBookParameters1 = AddBookParameters.createParameters() 134 | var addedBook1: Book! 135 | let addBook1CompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 136 | inMemoryCoreDataBooksGateway.add(parameters: addBookParameters1) { (result) in 137 | addedBook1 = try! result.get() 138 | addBook1CompletionHandlerExpectation.fulfill() 139 | } 140 | waitForExpectations(timeout: 1, handler: nil) 141 | 142 | let addBookParameters2 = AddBookParameters.createParameters() 143 | var addedBook2: Book! 144 | let addBook2CompletionHandlerExpectation = expectation(description: "Add book completion handler expectation") 145 | 146 | inMemoryCoreDataBooksGateway.add(parameters: addBookParameters2) { (result) in 147 | addedBook2 = try! result.get() 148 | addBook2CompletionHandlerExpectation.fulfill() 149 | } 150 | 151 | waitForExpectations(timeout: 1, handler: nil) 152 | 153 | let deleteBookCompletionHandlerExpectation = expectation(description: "Delete book completion handler expectation") 154 | 155 | // When 156 | inMemoryCoreDataBooksGateway.delete(book: addedBook1) { (result) in 157 | XCTAssertTrue(result == Result.success(()), "Expected a success result") 158 | deleteBookCompletionHandlerExpectation.fulfill() 159 | } 160 | 161 | waitForExpectations(timeout: 1, handler: nil) 162 | 163 | // Then 164 | let fetchBooksCompletionHandlerExpectation = expectation(description: "Fetch books completion handler expectation") 165 | inMemoryCoreDataBooksGateway.fetchBooks { (result) in 166 | let books = try! result.get() 167 | XCTAssertFalse(books.contains(addedBook1), "The added book should've been deleted") 168 | XCTAssertTrue(books.contains(addedBook2), "The second book should be contained") 169 | fetchBooksCompletionHandlerExpectation.fulfill() 170 | } 171 | 172 | waitForExpectations(timeout: 1, handler: nil) 173 | } 174 | 175 | func test_delete_fetch_fails() { 176 | // Book 177 | let bookToDelete = Book.createBook() 178 | let expectedResultToBeReturned: Result = .failure(CoreError(message: "Failed retrieving books the data base")) 179 | managedObjectContextSpy.fetchErrorToThrow = NSError.createError(withMessage: "Some core data error") 180 | 181 | let deleteBookCompletionHandlerExpectation = expectation(description: "Delete book completion handler expectation") 182 | 183 | // When 184 | errorPathCoreDataBooksGateway.delete(book: bookToDelete) { (result) in 185 | // Then 186 | XCTAssertTrue(expectedResultToBeReturned == result, "Failure error wasn't returned") 187 | deleteBookCompletionHandlerExpectation.fulfill() 188 | } 189 | 190 | waitForExpectations(timeout: 1, handler: nil) 191 | } 192 | 193 | func test_delete_save_fails() { 194 | // Book 195 | let bookToDelete = Book.createBook() 196 | let expectedResultToBeReturned: Result = .failure(CoreError(message: "Failed saving the context")) 197 | managedObjectContextSpy.entitiesToReturn = [inMemoryCoreDataStack.fakeEntity(withType: CoreDataBook.self)] 198 | managedObjectContextSpy.saveErrorToReturn = NSError.createError(withMessage: "Some core data error") 199 | 200 | let deleteBookCompletionHandlerExpectation = expectation(description: "Delete book completion handler expectation") 201 | 202 | // When 203 | errorPathCoreDataBooksGateway.delete(book: bookToDelete) { (result) in 204 | // Then 205 | XCTAssertTrue(expectedResultToBeReturned == result, "Failure error wasn't returned") 206 | deleteBookCompletionHandlerExpectation.fulfill() 207 | } 208 | 209 | waitForExpectations(timeout: 1, handler: nil) 210 | } 211 | } 212 | 213 | // MARK: - Helpers 214 | 215 | // https://www.bignerdranch.com/blog/creating-a-custom-xctest-assertion/ 216 | fileprivate func Assert(book: Book, builtFromParameters parameters: AddBookParameters, file: StaticString = #file, line: UInt = #line) { 217 | XCTAssertEqual(book.isbn, parameters.isbn, "isbn mismatch", file: file, line: line) 218 | XCTAssertEqual(book.title, parameters.title, "title mismatch", file: file, line: line) 219 | XCTAssertEqual(book.author, parameters.author, "author mismatch", file: file, line: line) 220 | XCTAssertEqual(book.releaseDate?.timeIntervalSince1970, parameters.releaseDate?.timeIntervalSince1970, "releaseDate mismatch", file: file, line: line) 221 | XCTAssertEqual(book.pages, parameters.pages, "pages mismatch", file: file, line: line) 222 | XCTAssertTrue(book.id != "", "id should not be empty", file: file, line: line) 223 | } 224 | 225 | fileprivate func Assert(book: Book, wasAddedIn coreDataBooksGateway: CoreDataBooksGateway, expectation: XCTestExpectation) { 226 | coreDataBooksGateway.fetchBooks { (result) in 227 | guard let books = try? result.get() else { 228 | XCTFail("Should've fetched the books with success") 229 | return 230 | } 231 | 232 | XCTAssertTrue(books.contains(book), "Book is not found in the returned books") 233 | XCTAssertEqual(books.count, 1, "Books array should contain exactly one book") 234 | expectation.fulfill() 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Creators/AddBookParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookParameters.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/1/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | extension AddBookParameters { 32 | static func createParameters() -> AddBookParameters { 33 | return AddBookParameters(isbn: "isbn", title: "title", author: "author", releaseDate: Date(), pages: 0) 34 | } 35 | } 36 | 37 | extension AddBookParameters: Equatable { } 38 | 39 | public func == (lhs: AddBookParameters, rhs: AddBookParameters) -> Bool { 40 | return lhs.isbn == rhs.isbn && lhs.title == rhs.title && lhs.author == rhs.author && lhs.releaseDate == rhs.releaseDate && lhs.pages == rhs.pages 41 | } 42 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Creators/Book.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Book.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | extension Book { 32 | static func createBooksArray(numberOfElements: Int = 2) -> [Book] { 33 | var books = [Book]() 34 | 35 | for i in 0.. Book { 44 | return Book(id: "\(index)", isbn: "ISBN \(index)", title: "Title \(index)", author: "Author \(index)", releaseDate: Date(), pages: index) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Creators/NSError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSError.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | extension NSError { 31 | static func createError(withMessage message: String) -> NSError { 32 | return NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Gateways/ApiBooksGatewaySpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiBooksGatewaySpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class ApiBooksGatewaySpy: ApiBooksGateway { 32 | var fetchBooksResultToBeReturned: Result<[Book]>! 33 | var addBookResultToBeReturned: Result! 34 | var deleteBookResultToBeReturned: Result! 35 | 36 | var addBookParameters: AddBookParameters! 37 | var deletedBook: Book! 38 | 39 | func fetchBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 40 | completionHandler(fetchBooksResultToBeReturned) 41 | } 42 | 43 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 44 | addBookParameters = parameters 45 | completionHandler(addBookResultToBeReturned) 46 | } 47 | 48 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 49 | deletedBook = book 50 | completionHandler(deleteBookResultToBeReturned) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Gateways/BooksGatewaySpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksGatewayStub.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class BooksGatewaySpy: BooksGateway { 32 | var fetchBooksResultToBeReturned: Result<[Book]>! 33 | var addBookResultToBeReturned: Result! 34 | var deleteBookResultToBeReturned: Result! 35 | 36 | var deletedBook: Book! 37 | 38 | func fetchBooks(completionHandler: @escaping FetchBooksEntityGatewayCompletionHandler) { 39 | 40 | } 41 | 42 | func add(parameters: AddBookParameters, completionHandler: @escaping AddBookEntityGatewayCompletionHandler) { 43 | 44 | } 45 | 46 | func delete(book: Book, completionHandler: @escaping DeleteBookEntityGatewayCompletionHandler) { 47 | deletedBook = book 48 | completionHandler(deleteBookResultToBeReturned) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Gateways/LocalPersistenceBooksGatewaySpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalPersistenceBooksGateway.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class LocalPersistenceBooksGatewaySpy: LocalPersistenceBooksGateway { 32 | var fetchBooksResultToBeReturned: Result<[Book]>! 33 | var addBookResultToBeReturned: Result! 34 | var deleteBookResultToBeReturned: Result! 35 | 36 | var addBookParameters: AddBookParameters! 37 | var deletedBook: Book! 38 | var booksSaved: [Book]! 39 | var addedBook: Book! 40 | 41 | var fetchBooksCalled = false 42 | 43 | func fetchBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 44 | fetchBooksCalled = true 45 | completionHandler(fetchBooksResultToBeReturned) 46 | } 47 | 48 | func add(parameters: AddBookParameters, completionHandler: @escaping (Result) -> Void) { 49 | addBookParameters = parameters 50 | completionHandler(addBookResultToBeReturned) 51 | } 52 | 53 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 54 | deletedBook = book 55 | completionHandler(deleteBookResultToBeReturned) 56 | } 57 | 58 | func save(books: [Book]) { 59 | booksSaved = books 60 | } 61 | 62 | func add(book: Book) { 63 | addedBook = book 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/InMemoryCoreDataStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryCoreDataStack.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/1/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | @testable import Library 32 | 33 | class InMemoryCoreDataStack: CoreDataStack { 34 | 35 | lazy var persistentContainer: NSPersistentContainer = { 36 | /* 37 | The persistent container for the application. This implementation 38 | creates and returns a container, having loaded the store for the 39 | application to it. This property is optional since there are legitimate 40 | error conditions that could cause the creation of the store to fail. 41 | */ 42 | let container = NSPersistentContainer(name: "Library") 43 | let persistentStoreDescription = NSPersistentStoreDescription() 44 | persistentStoreDescription.type = NSInMemoryStoreType 45 | 46 | container.persistentStoreDescriptions = [persistentStoreDescription] 47 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 48 | if let error = error as NSError? { 49 | // Replace this implementation with code to handle the error appropriately. 50 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 51 | 52 | /* 53 | Typical reasons for an error here include: 54 | * The parent directory does not exist, cannot be created, or disallows writing. 55 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 56 | * The device is out of space. 57 | * The store could not be migrated to the current model version. 58 | Check the error message to determine what the actual problem was. 59 | */ 60 | fatalError("Unresolved error \(error), \(error.userInfo)") 61 | } 62 | }) 63 | return container 64 | }() 65 | 66 | // MARK: - Core Data Saving support 67 | 68 | func saveContext () { 69 | let context = persistentContainer.viewContext 70 | if context.hasChanges { 71 | do { 72 | try context.save() 73 | } catch { 74 | // Replace this implementation with code to handle the error appropriately. 75 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 76 | let nserror = error as NSError 77 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 78 | } 79 | } 80 | } 81 | 82 | func fakeEntity(withType type: T.Type) -> T { 83 | return persistentContainer.viewContext.addEntity(withType: type)! 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/NSManagedObjectContextSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContextSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/2/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | import CoreData 30 | 31 | @testable import Library 32 | 33 | class NSManagedObjectContextSpy: NSManagedObjectContextProtocol { 34 | var fetchErrorToThrow: Error? 35 | var entitiesToReturn: [Any]? 36 | var addEntityToReturn: Any? 37 | var saveErrorToReturn: Error? 38 | var deletedObject: NSManagedObject? 39 | 40 | func allEntities(withType type: T.Type) throws -> [T] { 41 | return try allEntities(withType: type, predicate: nil) 42 | } 43 | 44 | func allEntities(withType type: T.Type, predicate: NSPredicate?) throws -> [T] { 45 | if let fetchErrorToThrow = fetchErrorToThrow { 46 | throw fetchErrorToThrow 47 | } else { 48 | return entitiesToReturn as! [T] 49 | } 50 | } 51 | 52 | func addEntity(withType type : T.Type) -> T? { 53 | return addEntityToReturn as? T 54 | } 55 | 56 | func save() throws { 57 | if let saveErrorToReturn = saveErrorToReturn { 58 | throw saveErrorToReturn 59 | } 60 | } 61 | 62 | func delete(_ object: NSManagedObject) { 63 | deletedObject = object 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Presenters/AddBookPresenterStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookPresenterStub.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class AddBookPresenterStub: AddBookPresenter { 32 | var router: AddBookViewRouter 33 | 34 | init (router: AddBookViewRouter) { 35 | self.router = router 36 | } 37 | 38 | var maximumReleaseDate = Date() 39 | 40 | func addButtonPressed(parameters: AddBookParameters) { } 41 | 42 | func cancelButtonPressed() { } 43 | } 44 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Routers/AddBookViewRouterSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookViewRouterSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class AddBookViewRouterSpy: AddBookViewRouter { 32 | var didCallDismiss = false 33 | 34 | func dismiss() { 35 | didCallDismiss = true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Routers/BooksViewRouterSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksViewRouterSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/27/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class BooksViewRouterSpy: BooksViewRouter { 32 | var passedBook: Book? 33 | var passedAddBookPresenterDelegate: AddBookPresenterDelegate? 34 | 35 | func presentDetailsView(for book: Book) { 36 | passedBook = book 37 | } 38 | 39 | func presentAddBook(addBookPresenterDelegate: AddBookPresenterDelegate) { 40 | passedAddBookPresenterDelegate = addBookPresenterDelegate 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/URLSessionStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionStub.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 3/2/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | 30 | @testable import Library 31 | 32 | class URLSessionStub: URLSessionProtocol { 33 | typealias URLSessionCompletionHandlerResponse = (data: Data?, response: URLResponse?, error: Error?) 34 | var responses = [URLSessionCompletionHandlerResponse]() 35 | 36 | func enqueue(response: URLSessionCompletionHandlerResponse) { 37 | responses.append(response) 38 | } 39 | 40 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 41 | return StubTask(response: responses.removeFirst(), completionHandler: completionHandler) 42 | } 43 | 44 | private class StubTask: URLSessionDataTask { 45 | let testDoubleResponse: URLSessionCompletionHandlerResponse 46 | let completionHandler: (Data?, URLResponse?, Error?) -> Void 47 | 48 | init(response: URLSessionCompletionHandlerResponse, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { 49 | self.testDoubleResponse = response 50 | self.completionHandler = completionHandler 51 | } 52 | 53 | override func resume() { 54 | completionHandler(testDoubleResponse.data, testDoubleResponse.response, testDoubleResponse.error) 55 | } 56 | } 57 | } 58 | 59 | extension URL { 60 | static var googleUrl: URL { 61 | return URL(string: "https://www.google.com")! 62 | } 63 | } 64 | 65 | extension HTTPURLResponse { 66 | convenience init(statusCode: Int) { 67 | self.init(url: URL.googleUrl, statusCode: statusCode, httpVersion: nil, headerFields: nil)! 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/UseCases/DeleteBookUseCaseSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteBookUseCaseSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/27/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class DeleteBookUseCaseSpy: DeleteBookUseCase { 32 | var resultToBeReturned: Result! 33 | var callCompletionHandlerImmediate = true 34 | var bookToDelete: Book? 35 | 36 | private var completionHandler: DeleteBookUseCaseCompletionHandler? 37 | 38 | func delete(book: Book, completionHandler: @escaping (Result) -> Void) { 39 | bookToDelete = book 40 | self.completionHandler = completionHandler 41 | 42 | if callCompletionHandlerImmediate { 43 | callCompletionHandler() 44 | } 45 | } 46 | 47 | func callCompletionHandler() { 48 | self.completionHandler?(resultToBeReturned) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/UseCases/DisplayBooksUseCaseStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBooksUseCaseStub.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/27/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class DisplayBooksUseCaseStub: DisplayBooksUseCase { 32 | var resultToBeReturned: Result<[Book]>! 33 | 34 | func displayBooks(completionHandler: @escaping (Result<[Book]>) -> Void) { 35 | completionHandler(resultToBeReturned) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Views/BookCellViewSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookCellViewSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/27/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class BookCellViewSpy: BookCellView { 32 | var displayedTitle = "" 33 | var displayedAuthor = "" 34 | var displayedReleaseDate = "" 35 | 36 | func display(title: String) { 37 | displayedTitle = title 38 | } 39 | 40 | func display(author: String) { 41 | displayedAuthor = author 42 | } 43 | 44 | func display(releaseDate: String) { 45 | displayedReleaseDate = releaseDate 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LibraryTests/Helpers/Views/BooksViewSpy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksViewSpy.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/27/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | class BooksViewSpy: BooksView { 32 | var refreshBooksViewCalled = false 33 | var displayBooksRetrievalErrorTitle: String? 34 | var displayBooksRetrievalErrorMessage: String? 35 | var displayBookDeleteErrorTitle: String? 36 | var displayBookDeleteErrorMessage: String? 37 | var deletedRow: Int? 38 | var endEditingCalled = false 39 | 40 | func refreshBooksView() { 41 | refreshBooksViewCalled = true 42 | } 43 | 44 | func displayBooksRetrievalError(title: String, message: String) { 45 | displayBooksRetrievalErrorTitle = title 46 | displayBooksRetrievalErrorMessage = message 47 | } 48 | 49 | func displayBookDeleteError(title: String, message: String) { 50 | displayBookDeleteErrorTitle = title 51 | displayBookDeleteErrorMessage = message 52 | } 53 | 54 | func deleteAnimated(row: Int) { 55 | deletedRow = row 56 | } 57 | 58 | func endEditing() { 59 | endEditingCalled = true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LibraryTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LibraryTests/Presenters/BooksPresenterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooksPresenterTest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/24/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import XCTest 29 | @testable import Library 30 | 31 | class BooksPresenterTest: XCTestCase { 32 | // https://www.martinfowler.com/bliki/TestDouble.html 33 | let diplayBooksUseCaseStub = DisplayBooksUseCaseStub() 34 | let deleteBookUseCaseSpy = DeleteBookUseCaseSpy() 35 | let booksViewRouterSpy = BooksViewRouterSpy() 36 | let booksViewSpy = BooksViewSpy() 37 | 38 | var booksPresenter: BooksPresenterImplementation! 39 | 40 | // MARK: - Set up 41 | 42 | override func setUp() { 43 | super.setUp() 44 | booksPresenter = BooksPresenterImplementation(view: booksViewSpy, 45 | displayBooksUseCase: diplayBooksUseCaseStub, 46 | deleteBookUseCase: deleteBookUseCaseSpy, 47 | router: booksViewRouterSpy) 48 | } 49 | 50 | // MARK: - Tests 51 | 52 | func test_viewDidLoad_success_refreshBooksView_called() { 53 | // Given 54 | let booksToBeReturned = Book.createBooksArray() 55 | diplayBooksUseCaseStub.resultToBeReturned = .success(booksToBeReturned) 56 | 57 | // When 58 | booksPresenter.viewDidLoad() 59 | 60 | // Then 61 | XCTAssertTrue(booksViewSpy.refreshBooksViewCalled, "refreshBooksView was not called") 62 | } 63 | 64 | func test_viewDidLoad_success_numberOfBooks() { 65 | // Given 66 | let expectedNumberOfBooks = 5 67 | let booksToBeReturned = Book.createBooksArray(numberOfElements: expectedNumberOfBooks) 68 | diplayBooksUseCaseStub.resultToBeReturned = .success(booksToBeReturned) 69 | 70 | // When 71 | booksPresenter.viewDidLoad() 72 | 73 | // Then 74 | XCTAssertEqual(expectedNumberOfBooks, booksPresenter.numberOfBooks, "Number of books mismatch") 75 | } 76 | 77 | func test_viewDidLoad_failure_displayBooksRetrievalError() { 78 | // Given 79 | let expectedErrorTitle = "Error" 80 | let expectedErrorMessage = "Some error message" 81 | let errorToBeReturned = NSError.createError(withMessage: expectedErrorMessage) 82 | diplayBooksUseCaseStub.resultToBeReturned = .failure(errorToBeReturned) 83 | 84 | // When 85 | booksPresenter.viewDidLoad() 86 | 87 | // Then 88 | XCTAssertEqual(expectedErrorTitle, booksViewSpy.displayBooksRetrievalErrorTitle, "Error title doesn't match") 89 | XCTAssertEqual(expectedErrorMessage, booksViewSpy.displayBooksRetrievalErrorMessage, "Error message doesn't match") 90 | } 91 | 92 | func test_configureCell_has_release_date() { 93 | // Given 94 | booksPresenter.books = Book.createBooksArray() 95 | let expectedDisplayedTitle = "Title 1" 96 | let expectedDisplayedAuthor = "Author 1" 97 | let expectedDisplayedReleaseDate = "Long time ago" 98 | 99 | let bookCellViewSpy = BookCellViewSpy() 100 | 101 | // When 102 | booksPresenter.configure(cell: bookCellViewSpy, forRow: 1) 103 | 104 | // Then 105 | XCTAssertEqual(expectedDisplayedTitle, bookCellViewSpy.displayedTitle, "The title we expected was not displayed") 106 | XCTAssertEqual(expectedDisplayedAuthor, bookCellViewSpy.displayedAuthor, "The author we expected was not displayed") 107 | XCTAssertEqual(expectedDisplayedReleaseDate, bookCellViewSpy.displayedReleaseDate, "The date we expected was not displayed") 108 | } 109 | 110 | func test_configureCell_release_date_nil() { 111 | // Given 112 | let rowToConfigure = 1 113 | booksPresenter.books = Book.createBooksArray() 114 | booksPresenter.books[rowToConfigure].releaseDate = nil 115 | let expectedDisplayedTitle = "Title 1" 116 | let expectedDisplayedAuthor = "Author 1" 117 | let expectedDisplayedReleaseDate = "Unknown" 118 | 119 | let bookCellViewSpy = BookCellViewSpy() 120 | 121 | // When 122 | booksPresenter.configure(cell: bookCellViewSpy, forRow: rowToConfigure) 123 | 124 | // Then 125 | XCTAssertEqual(expectedDisplayedTitle, bookCellViewSpy.displayedTitle, "The title we expected was not displayed") 126 | XCTAssertEqual(expectedDisplayedAuthor, bookCellViewSpy.displayedAuthor, "The author we expected was not displayed") 127 | XCTAssertEqual(expectedDisplayedReleaseDate, bookCellViewSpy.displayedReleaseDate, "The date we expected was not displayed") 128 | } 129 | 130 | func test_didSelect_navigates_to_details_view() { 131 | // Given 132 | let books = Book.createBooksArray() 133 | let rowToSelect = 1 134 | booksPresenter.books = books 135 | 136 | // When 137 | booksPresenter.didSelect(row: rowToSelect) 138 | 139 | // Then 140 | XCTAssertEqual(books[rowToSelect], booksViewRouterSpy.passedBook, "Expected navigate to details view to be called with book at index 1") 141 | } 142 | 143 | func test_canEdit_always_returns_true() { 144 | // When 145 | let anyRow = 0 146 | let canEdit = booksPresenter.canEdit(row: anyRow) 147 | 148 | // Then 149 | XCTAssertTrue(canEdit, "Can edit should always return true") 150 | } 151 | 152 | func test_titleForDeleteButton_same_for_all_indexes() { 153 | // Given 154 | let expectedTitle = "Delete Book" 155 | let anyRow = 0 156 | 157 | // When 158 | let actualTitle = booksPresenter.titleForDeleteButton(row: anyRow) 159 | 160 | // Then 161 | XCTAssertEqual(expectedTitle, actualTitle, "The title for delete button doesn't match") 162 | } 163 | 164 | func test_deleteButtonPressed_endEditing_called_before_delete_completion_handler() { 165 | // Given 166 | let rowToDelete = 1 167 | let books = Book.createBooksArray() 168 | booksPresenter.books = books 169 | deleteBookUseCaseSpy.callCompletionHandlerImmediate = false 170 | 171 | // When 172 | booksPresenter.deleteButtonPressed(row: rowToDelete) 173 | 174 | // Then 175 | XCTAssertTrue(booksViewSpy.endEditingCalled, "End editing should've been called") 176 | XCTAssertEqual(nil, booksViewSpy.deletedRow, "Delete row on the view shouldn't have been called because we didn't run the completion handler from the use case") 177 | XCTAssertEqual(nil, booksViewSpy.displayBookDeleteErrorTitle, "Display error on the view shouldn't have been called because we didn't run the completion handler from the use case") 178 | XCTAssertEqual(nil, booksViewSpy.displayBookDeleteErrorMessage, "Display error on the view shouldn't have been called because we didn't run the completion handler from the use case") 179 | } 180 | 181 | func test_deleteButtonPressed_sucess_view_is_updated() { 182 | // Given 183 | let rowToDelete = 1 184 | let books = Book.createBooksArray() 185 | let bookToDelete = books[rowToDelete] 186 | booksPresenter.books = books 187 | deleteBookUseCaseSpy.resultToBeReturned = .success(()) 188 | 189 | // When 190 | booksPresenter.deleteButtonPressed(row: rowToDelete) 191 | 192 | // Then 193 | XCTAssertEqual(bookToDelete, deleteBookUseCaseSpy.bookToDelete, "Book at wrong index was passed to be deleted") 194 | XCTAssertEqual(rowToDelete, booksViewSpy.deletedRow, "Delete on the view should have been called") 195 | XCTAssertFalse(booksPresenter.books.contains(bookToDelete), "Book should have been deleted") 196 | } 197 | 198 | func test_deleteButtonPressed_success_book_already_deleted_view_shouldnt_be_updated() { 199 | // Given 200 | let rowToDelete = 1 201 | let books = Book.createBooksArray() 202 | booksPresenter.books = books 203 | deleteBookUseCaseSpy.resultToBeReturned = .success(()) 204 | deleteBookUseCaseSpy.callCompletionHandlerImmediate = false 205 | 206 | // When 207 | booksPresenter.deleteButtonPressed(row: rowToDelete) 208 | booksPresenter.books.remove(at: rowToDelete) 209 | deleteBookUseCaseSpy.callCompletionHandler() 210 | 211 | // Then 212 | XCTAssertEqual(books[rowToDelete], deleteBookUseCaseSpy.bookToDelete, "Book at wrong index was passed to be deleted") 213 | XCTAssertEqual(nil, booksViewSpy.deletedRow, "Delete on the view shouldn't have been called") 214 | } 215 | 216 | func test_deleteButtonPressed_failure_view_displays_error() { 217 | // Given 218 | let rowToDelete = 1 219 | let books = Book.createBooksArray() 220 | booksPresenter.books = books 221 | let expectedErrorMessage = "Some delete book error message" 222 | deleteBookUseCaseSpy.resultToBeReturned = .failure(NSError.createError(withMessage: expectedErrorMessage)) 223 | 224 | // When 225 | booksPresenter.deleteButtonPressed(row: rowToDelete) 226 | 227 | // Then 228 | XCTAssertEqual(books[rowToDelete], deleteBookUseCaseSpy.bookToDelete, "Book at wrong index was passed to be deleted") 229 | XCTAssertEqual("Error", booksViewSpy.displayBookDeleteErrorTitle, "Error titlex doesn't match") 230 | XCTAssertEqual(expectedErrorMessage, booksViewSpy.displayBookDeleteErrorMessage, "Error message doesn't match") 231 | } 232 | 233 | func test_deleteBookUseCase_notification_deletes_book() { 234 | // Given 235 | let rowToDelete = 1 236 | let books = Book.createBooksArray() 237 | let bookToDelete = books[rowToDelete] 238 | booksPresenter.books = books 239 | 240 | // When 241 | NotificationCenter.default.post(name: DeleteBookUseCaseNotifications.didDeleteBook, object: bookToDelete) 242 | 243 | // Then 244 | XCTAssertEqual(rowToDelete, booksViewSpy.deletedRow, "Delete on the view should have been called") 245 | XCTAssertFalse(booksPresenter.books.contains(bookToDelete), "Book should have been deleted") 246 | } 247 | 248 | func test_deleteBookUseCase_notification_book_already_deleted_view_shouldnt_be_updated() { 249 | // Given 250 | let rowToDelete = 1 251 | let books = Book.createBooksArray() 252 | let bookToDelete = books[rowToDelete] 253 | 254 | booksPresenter.books = books 255 | booksPresenter.books.remove(at: rowToDelete) 256 | 257 | // When 258 | NotificationCenter.default.post(name: DeleteBookUseCaseNotifications.didDeleteBook, object: bookToDelete) 259 | 260 | // Then 261 | XCTAssertEqual(nil, booksViewSpy.deletedRow, "Delete on the view shouldn't have been called") 262 | } 263 | 264 | func test_addButtonPressed_navigates_to_add_book_view() { 265 | // When 266 | booksPresenter.addButtonPressed() 267 | 268 | // Then 269 | XCTAssertTrue(booksPresenter === booksViewRouterSpy.passedAddBookPresenterDelegate, "BooksPresenter wasn't passed as delegate to BooksViewRouter") 270 | } 271 | 272 | func test_addBookPresenter_didAdd_book_refreshBooksView_called() { 273 | // Given 274 | let addedBook = Book.createBook() 275 | let addBookViewRouterSpy = AddBookViewRouterSpy() 276 | let addBookPresenterStub = AddBookPresenterStub(router: addBookViewRouterSpy) 277 | 278 | // When 279 | booksPresenter.addBookPresenter(addBookPresenterStub, didAdd: addedBook) 280 | 281 | // Then 282 | XCTAssertTrue(booksPresenter.books.contains(addedBook), "Book wasn't added in the presenter") 283 | XCTAssertTrue(addBookViewRouterSpy.didCallDismiss, "Dismiss wasn't called on the AddBookViewRouter") 284 | XCTAssertTrue(booksViewSpy.refreshBooksViewCalled, "refreshBooksView was not called") 285 | } 286 | 287 | func test_addBookPresenterCancel_dismiss_view() { 288 | // Given 289 | let addBookViewRouterSpy = AddBookViewRouterSpy() 290 | let addBookPresenterStub = AddBookPresenterStub(router: addBookViewRouterSpy) 291 | 292 | // When 293 | booksPresenter.addBookPresenterCancel(presenter: addBookPresenterStub) 294 | 295 | // Then 296 | XCTAssertTrue(addBookViewRouterSpy.didCallDismiss, "Dismiss wasn't called on the AddBookViewRouter") 297 | } 298 | } 299 | 300 | 301 | -------------------------------------------------------------------------------- /LibraryTests/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import Foundation 29 | @testable import Library 30 | 31 | public func ==(lhs: Result, rhs: Result) -> Bool { 32 | // Shouldn't be used for PRODUCTION enum comparison. Good enough for unit tests. 33 | return String(describing: lhs) == String(describing: rhs) 34 | } 35 | -------------------------------------------------------------------------------- /LibraryTests/UseCases/DeleteBookUseCaseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteBookUseCaseTest.swift 3 | // Library 4 | // 5 | // Created by Cosmin Stirbu on 2/28/17. 6 | // MIT License 7 | // 8 | // Copyright (c) 2017 Fortech 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | import XCTest 29 | 30 | @testable import Library 31 | 32 | class DeleteBookUseCaseTest: XCTestCase { 33 | // https://www.martinfowler.com/bliki/TestDouble.html 34 | let booksGatewaySpy = BooksGatewaySpy() 35 | 36 | var deleteBookUseCase: DeleteBookUseCaseImplementation! 37 | 38 | // MARK: - Set up 39 | 40 | override func setUp() { 41 | super.setUp() 42 | deleteBookUseCase = DeleteBookUseCaseImplementation(booksGateway: booksGatewaySpy) 43 | } 44 | 45 | // MARK: - Tests 46 | 47 | func test_delete_success_sends_notification_calls_completion_handler() { 48 | // Given 49 | let bookToDelete = Book.createBook() 50 | let expectedResultToBeReturned: Result = Result.success(()) 51 | booksGatewaySpy.deleteBookResultToBeReturned = expectedResultToBeReturned 52 | 53 | let deleteBookCompletionHandlerExpectation = expectation(description: "Delete Book Expectation") 54 | let _ = expectation(forNotification: NSNotification.Name(rawValue: DeleteBookUseCaseNotifications.didDeleteBook.rawValue), object: nil, handler: nil) 55 | 56 | // When 57 | deleteBookUseCase.delete(book: bookToDelete) { (result) in 58 | // Then 59 | XCTAssertTrue(expectedResultToBeReturned == result, "Completion handler didn't return the expected result") 60 | XCTAssertEqual(bookToDelete, self.booksGatewaySpy.deletedBook, "Incorrect book passed to the gateway") 61 | deleteBookCompletionHandlerExpectation.fulfill() 62 | } 63 | 64 | waitForExpectations(timeout: 1, handler: nil) 65 | } 66 | 67 | func test_delete_failure_calls_completion_handler() { 68 | // Given 69 | let bookToDelete = Book.createBook() 70 | let expectedResultToBeReturned: Result = Result.failure(NSError.createError(withMessage: "Any message")) 71 | booksGatewaySpy.deleteBookResultToBeReturned = expectedResultToBeReturned 72 | 73 | let deleteBookCompletionHandlerExpectation = expectation(description: "Delete Book Expectation") 74 | 75 | // When 76 | deleteBookUseCase.delete(book: bookToDelete) { (result) in 77 | // Then 78 | XCTAssertTrue(expectedResultToBeReturned == result, "Completion handler didn't return the expected result") 79 | XCTAssertEqual(bookToDelete, self.booksGatewaySpy.deletedBook, "Incorrect book passed to the gateway") 80 | deleteBookCompletionHandlerExpectation.fulfill() 81 | } 82 | 83 | waitForExpectations(timeout: 1, handler: nil) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Library - iOS - MVP + Clean Architecture Demo 2 | 3 | ### Description 4 | *Library* is an iOS application built to highlight __MVP (Model View Presenter)__ and __Clean Architecture__ concepts 5 | 6 | ### Run Requirements 7 | 8 | * Xcode 10.2.1 9 | * Swift 5 10 | 11 | ### High Level Layers 12 | 13 | #### MVP Concepts 14 | ##### Presentation Logic 15 | * `View` - delegates user interaction events to the `Presenter` and displays data passed by the `Presenter` 16 | * All `UIViewController`, `UIView`, `UITableViewCell` subclasses belong to the `View` layer 17 | * Usually the view is passive / dumb - it shouldn't contain any complex logic and that's why most of the times we don't need write Unit Tests for it 18 | * `Presenter` - contains the presentation logic and tells the `View` what to present 19 | * Usually we have one `Presenter` per scene (view controller) 20 | * It doesn't reference the concrete type of the `View`, but rather it references the `View` protocol that is implemented usually by a `UIViewController` subclass 21 | * It should be a plain `Swift` class and not reference any `iOS` framework classes - this makes it easier to reuse it maybe in a `macOS` application 22 | * It should be covered by Unit Tests 23 | * `Configurator` - injects the dependency object graph into the scene (view controller) 24 | * You could very easily use a DI (dependency injection) library. Unfortunately DI libraries are not quite mature yet on `iOS` / `Swift` 25 | * Usually it contains very simple logic and we don't need to write Unit Tests for it 26 | * `Router` - contains navigation / flow logic from one scene (view controller) to another 27 | * In some communities / blog posts it might be referred to as a `FlowController` 28 | * Writing tests for it is quite difficult because it contains many references to `iOS` framework classes so usually we try to keep it really simple and we don't write Unit Tests for it 29 | * It is usually referenced only by the `Presenter` but due to the `func prepare(for segue: UIStoryboardSegue, sender: Any?)` method we some times need to reference it in the view controller as well 30 | 31 | #### Clean Architecture Concepts 32 | ##### Application Logic 33 | 34 | * `UseCase / Interactor` - contains the application / business logic for a specific use case in your application 35 | * It is referenced by the `Presenter`. The `Presenter` can reference multiple `UseCases` since it's common to have multiple use cases on the same screen 36 | * It manipulates `Entities` and communicates with `Gateways` to retrieve / persist the entities 37 | * The `Gateway` protocols should be defined in the `Application Logic` layers and implemented by the `Gateways & Framework Logic` 38 | * The separation described above ensures that the `Application Logic` depends on abstractions and not on actual frameworks / implementations 39 | * It should be covered by Unit Tests 40 | * `Entity` - plain `Swift` classes / structs 41 | * Models objects used by your application such as `Order`, `Product`, `Shopping Cart`, etc 42 | 43 | ##### Gateways & Framework Logic 44 | 45 | * `Gateway` - contains actual implementation of the protocols defined in the `Application Logic` layer 46 | * We can implement for instance a `LocalPersistenceGateway` protocol using `CoreData` or `Realm` 47 | * We can implement for instance an `ApiGateway` protocol using `URLSession` or `Alamofire` 48 | * We can implement for instance a `UserSettings` protocol using `UserDefaults` 49 | * It should be covered by Unit Tests 50 | * `Persistence / API Entities` - contains framework specific representations 51 | * For instance we could have a `CoreDataOrder` that is a `NSManagedObject` subclass 52 | * The `CoreDataOrder` would not be passed to the `Application Logic` layer but rather the `Gateways & Framework Logic` layer would have to "transform" it to an `Order` entity defined in the `Application Logic` layer 53 | * `Framework specific APIs` - contains implementations of `iOS` specific APIs such as sensors / bluetooth / camera 54 | 55 | ### Demo Application Details 56 | 57 | * The demo applications tries to expose a fairly complex set of features that justifies the usage of the concepts presented above 58 | * The following __Unit Tests__ have been written: 59 | * `BooksPresenterTest` - highlights how you can test the presentation logic 60 | * `DeleteBookUseCaseTest` - highlights how you can test the application / business logic and also how to test async code that uses completion handlers and `NotificationCenter` 61 | * `CacheBooksGatewayTest` - highlights how you can test a cache policy 62 | * `CoreDataBooksGatewayTest` - highlights how you can test a `CoreData` gateway 63 | * `ApiClientTest` - highlights how you can test the API / Networking layer of your application by substituting the `URLSession` stack 64 | * __Code comments__ can be found in several classes highlighting different design decisions or referencing followup resources 65 | * The project structure tries to mimic the __Screaming Architecture__ concept that can be found in the references section 66 | * High level UML diagram: 67 | ![High level UML diagram](CleanArchitecture.png) 68 | 69 | ### Debatable Design Decisions 70 | 71 | Giving that a large majority of mobile apps are a thin client on top of a set of APIs and that most of them contain little business logic (since most of the business logic is found in the APIs) some of the `Clean Architecture` concepts can be debatable in the mobile world. Below you can find some: 72 | 73 | * Creating a representation for each layer (API, CoreData) might seem like over-engineering. If your application relies heavily on an API that is under your control then it might make sense to model both the entity and the API representation using the same class. You shouldn't however allow the persistence representation (the `NSManagedObject` subclass for instance) leak in the other layers (see [`Parse`](https://techcrunch.com/2016/01/28/facebook-shutters-its-parse-developer-platform/) example that got discontinued) 74 | * If you find that in most cases your `Use Cases / Interactors` simply delegate the actions to the `Gateway` then maybe you don't need the `Use Cases / Interactors` in the first place and you can use the `Gateway` directly in the `Presenter` 75 | * If you want to enforce the layer separation even more you can consider moving all the layers in their own projects / modules 76 | * Some might consider that creating `display(xyz: String)` methods on a `CellView` protocol is over-engineering and that passing a plane `CellViewModel` object to the `CellView` and have the view configure itself with the view model is more straightforward. If you want top keep the view as passive / dumb as possible then you should probably create the methods, but then again simply reading some strings from a view model and setting some labels is not really complex logic 77 | 78 | The list above is definitely not complete, and if you identify other debatable decisions please create an issue and we can discuss about it and include it in the list above. 79 | 80 | For the items listed above (and also for other items of your own) it is important that you use __your own judgement__ and make an __informed decision__. 81 | 82 | Keep in mind that you don't have to make all the design decisions up front and that you can refactor them in as you go. 83 | 84 | Discuss about all the design decision with your team members and make sure you are all in agreement. 85 | 86 | ### Useful Resources 87 | 88 | #### MVP & Other presentation patterns 89 | 90 | * [iOS Architecture Patterns](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.67lieoiim) 91 | * [Architecture Wars - A New Hope](https://swifting.io/blog/2016/09/07/architecture-wars-a-new-hope/) 92 | * [VIPER to be or not to be?](https://swifting.io/blog/2016/03/07/8-viper-to-be-or-not-to-be/?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post) 93 | * [Effective Android Architecture](https://realm.io/news/360andev-richa-khandelwal-effective-android-architecture-patterns-java/) - our note here is that you should be careful about coupling you application to Rx* or any other framework for that matter. Please read [Make the Magic go away, by Uncle Bob](https://8thlight.com/blog/uncle-bob/2015/08/06/let-the-magic-die.html) and think twice before letting a framework take over your application. 94 | * [Improve your iOS Architecture with FlowControllers](http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/) 95 | * [GUI Architectures, by Martin Fowler](https://martinfowler.com/eaaDev/uiArchs.html) 96 | 97 | #### Clean Architecture 98 | * [The Clean Architecture, by Uncle Bob](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) 99 | * [Architecture: The Lost Years, by Uncle Bob](https://www.youtube.com/watch?v=HhNIttd87xs) 100 | * [Clean Architecture, By Uncle Bob](https://8thlight.com/blog/uncle-bob/2011/11/22/Clean-Architecture.html) 101 | * [Uncle Bob's clean architecture - An entity/model class for each layer?](http://softwareengineering.stackexchange.com/questions/303478/uncle-bobs-clean-architecture-an-entity-model-class-for-each-layer) 102 | 103 | #### Unit Tests 104 | * [xUnit Test Patterns: Refactoring Test Code](https://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054) 105 | * [The Art of Unit Testing: with examples in C#](https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890/) 106 | 107 | ### Contributing 108 | 109 | Please feel free to open an issue for any questions or suggestions you have! 110 | --------------------------------------------------------------------------------