├── .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 | 
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 |
--------------------------------------------------------------------------------