├── README.md ├── Gister.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── Gister ├── Gister.xcdatamodeld │ ├── .xccurrentversion │ └── Gister.xcdatamodel │ │ └── contents ├── Scenes │ └── ListGists │ │ ├── ListGistsModels.swift │ │ ├── ListGistsWorker.swift │ │ ├── ListGistsPresenter.swift │ │ ├── ListGistsInteractor.swift │ │ ├── ListGistsRouter.swift │ │ └── ListGistsViewController.swift ├── APIs │ ├── Gist.swift │ └── GistAPI.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.swift ├── GisterTests ├── Gist.json ├── Info.plist ├── GisterTests.swift ├── Seeds.swift ├── Scenes │ └── ListGists │ │ ├── ListGistsPresenterTests.swift │ │ ├── ListGistsViewControllerTests.swift │ │ ├── ListGistsWorkerTests.swift │ │ └── ListGistsInteractorTests.swift └── APIs │ └── GistAPITest.swift ├── GisterUITests ├── Info.plist └── GisterUITests.swift └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # Gister 2 | This app lists all public gists using GitHub's API. It demonstrates how to encapsulate all API code and write unit tests for it. 3 | -------------------------------------------------------------------------------- /Gister.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Gister/Gister.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Gister.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gister.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gister/Gister.xcdatamodeld/Gister.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /GisterTests/Gist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "owner": { 4 | "login": "amy" 5 | }, 6 | "files": { 7 | "amy.txt": { 8 | "filename": "amy.txt", 9 | "type": "text/plain" 10 | } 11 | }, 12 | "html_url": "https://gist.github.com/rayvinly/27e1cc51efc3a1015a1e" 13 | }, 14 | { 15 | "owner": { 16 | "login": "bob" 17 | }, 18 | "files": { 19 | "bob.html": { 20 | "filename": "bob.html", 21 | "type": "text/html" 22 | } 23 | }, 24 | "html_url": "https://gist.github.com/rayvinly/46396696f020c3e0931c" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /GisterTests/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 | -------------------------------------------------------------------------------- /GisterUITests/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 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsModels.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | enum ListGists 16 | { 17 | // MARK: Use cases 18 | 19 | enum FetchGists 20 | { 21 | struct Request 22 | { 23 | } 24 | struct Response 25 | { 26 | var gists: [Gist] 27 | } 28 | struct ViewModel 29 | { 30 | struct DisplayedGist 31 | { 32 | var login: String 33 | var url: String 34 | var filename: String 35 | var filetype: String 36 | } 37 | var displayedGists: [DisplayedGist] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GisterTests/GisterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GisterTests.swift 3 | // GisterTests 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Gister 11 | 12 | class GisterTests: XCTestCase 13 | { 14 | override func setUp() 15 | { 16 | super.setUp() 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() 21 | { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | super.tearDown() 24 | } 25 | 26 | func testExample() 27 | { 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | func testPerformanceExample() 33 | { 34 | // This is an example of a performance test case. 35 | self.measure { 36 | // Put the code you want to measure the time of here. 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsWorker.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | protocol ListGistsWorkerDelegate 16 | { 17 | func listGistsWorker(listGistsWorker: ListGistsWorker, didFetchGists gists: [Gist]) 18 | } 19 | 20 | class ListGistsWorker: GistAPIDelegate 21 | { 22 | var gistAPI: GistAPIProtocol = GistAPI() 23 | var delegate: ListGistsWorkerDelegate? 24 | 25 | // MARK: Block implementation 26 | 27 | func fetch(completionHandler: @escaping ([Gist]) -> Void) 28 | { 29 | gistAPI.fetch { (gists) in 30 | completionHandler(gists) 31 | } 32 | } 33 | 34 | // MARK: Delegate implementation 35 | 36 | func fetch() 37 | { 38 | gistAPI.delegate = self 39 | gistAPI.fetch() 40 | } 41 | 42 | func gistAPI(gistAPI: GistAPIProtocol, didFetchGists gists: [Gist]) 43 | { 44 | delegate?.listGistsWorker(listGistsWorker: self, didFetchGists: gists) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Gister/APIs/Gist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gist.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/21/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Gist 12 | { 13 | var login: String 14 | var url: String 15 | var filename: String 16 | var filetype: String 17 | } 18 | 19 | extension Gist: Equatable 20 | { 21 | static func ==(lhs: Gist, rhs: Gist) -> Bool 22 | { 23 | return lhs.login == rhs.login && 24 | lhs.url == rhs.url && 25 | lhs.filename == rhs.filename && 26 | lhs.filetype == rhs.filetype 27 | } 28 | } 29 | 30 | extension Gist: Hashable 31 | { 32 | var hashValue: Int 33 | { 34 | return login.hashValue 35 | } 36 | } 37 | 38 | extension ListGists.FetchGists.ViewModel.DisplayedGist: Equatable 39 | { 40 | static func ==(lhs: ListGists.FetchGists.ViewModel.DisplayedGist, rhs: ListGists.FetchGists.ViewModel.DisplayedGist) -> Bool 41 | { 42 | return lhs.login == rhs.login && 43 | lhs.url == rhs.url && 44 | lhs.filename == rhs.filename && 45 | lhs.filetype == rhs.filetype 46 | } 47 | } 48 | 49 | extension ListGists.FetchGists.ViewModel.DisplayedGist: Hashable 50 | { 51 | var hashValue: Int 52 | { 53 | return login.hashValue 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsPresenter.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | protocol ListGistsPresentationLogic 16 | { 17 | func presentFetchedGists(response: ListGists.FetchGists.Response) 18 | } 19 | 20 | class ListGistsPresenter: ListGistsPresentationLogic 21 | { 22 | weak var viewController: ListGistsDisplayLogic? 23 | 24 | // MARK: Fetch Gists 25 | 26 | func presentFetchedGists(response: ListGists.FetchGists.Response) 27 | { 28 | let displayedGists = convertGists(gists: response.gists) 29 | let viewModel = ListGists.FetchGists.ViewModel(displayedGists: displayedGists) 30 | viewController?.displayFetchedGists(viewModel: viewModel) 31 | } 32 | 33 | private func convertGists(gists: [Gist]) -> [ListGists.FetchGists.ViewModel.DisplayedGist] 34 | { 35 | return gists.map { ListGists.FetchGists.ViewModel.DisplayedGist(login: $0.login, url: $0.url, filename: $0.filename, filetype: $0.filetype) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GisterUITests/GisterUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GisterUITests.swift 3 | // GisterUITests 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class GisterUITests: XCTestCase 12 | { 13 | override func setUp() 14 | { 15 | super.setUp() 16 | 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | 19 | // In UI tests it is usually best to stop immediately when a failure occurs. 20 | continueAfterFailure = false 21 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 22 | XCUIApplication().launch() 23 | 24 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 25 | } 26 | 27 | override func tearDown() 28 | { 29 | // Put teardown code here. This method is called after the invocation of each test method in the class. 30 | super.tearDown() 31 | } 32 | 33 | func testExample() 34 | { 35 | // Use recording to get started writing UI tests. 36 | // Use XCTAssert and related functions to verify your tests produce the correct results. 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Gister/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 | } -------------------------------------------------------------------------------- /GisterTests/Seeds.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seeds.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/20/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | @testable import Gister 10 | import XCTest 11 | 12 | struct Seeds 13 | { 14 | struct Gists 15 | { 16 | static let text = Gist(login: "amy", url: "https://gist.github.com/rayvinly/27e1cc51efc3a1015a1e", filename: "amy.txt", filetype: "text/plain") 17 | static let html = Gist(login: "bob", url: "https://gist.github.com/rayvinly/46396696f020c3e0931c", filename: "bob.html", filetype: "text/html") 18 | } 19 | 20 | struct DisplayedGists 21 | { 22 | static let text = ListGists.FetchGists.ViewModel.DisplayedGist(login: Gists.text.login, url: Gists.text.url, filename: Gists.text.filename, filetype: Gists.text.filetype) 23 | static let html = ListGists.FetchGists.ViewModel.DisplayedGist(login: Gists.html.login, url: Gists.html.url, filename: Gists.html.filename, filetype: Gists.html.filetype) 24 | } 25 | 26 | struct JSON { 27 | static let data: Data = 28 | { 29 | let bundle = Bundle(identifier: "com.clean-swift.GisterTests")! 30 | let path = bundle.path(forResource: "Gist", ofType: "json")! 31 | let data = FileManager.default.contents(atPath: path)! 32 | let json = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) 33 | return data 34 | }() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Gister/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 | 45 | 46 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsInteractor.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | protocol ListGistsBusinessLogic 16 | { 17 | func fetchGists(request: ListGists.FetchGists.Request) 18 | } 19 | 20 | protocol ListGistsDataStore 21 | { 22 | } 23 | 24 | class ListGistsInteractor: ListGistsBusinessLogic, ListGistsDataStore, ListGistsWorkerDelegate 25 | { 26 | var presenter: ListGistsPresentationLogic? 27 | var listGistsWorker = ListGistsWorker() 28 | 29 | enum AsyncOpKind { 30 | case block, delegate 31 | } 32 | var asyncOpKind = AsyncOpKind.block 33 | 34 | // MARK: Fetch Gists 35 | 36 | func fetchGists(request: ListGists.FetchGists.Request) 37 | { 38 | switch asyncOpKind 39 | { 40 | case .block: 41 | // MARK: Block implementation 42 | listGistsWorker.fetch { (gists) in 43 | let response = ListGists.FetchGists.Response(gists: gists) 44 | self.presenter?.presentFetchedGists(response: response) 45 | } 46 | case .delegate: 47 | // MARK: Delegate method implementation 48 | listGistsWorker.delegate = self 49 | listGistsWorker.fetch() 50 | } 51 | } 52 | 53 | func listGistsWorker(listGistsWorker: ListGistsWorker, didFetchGists gists: [Gist]) 54 | { 55 | let response = ListGists.FetchGists.Response(gists: gists) 56 | presenter?.presentFetchedGists(response: response) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /Gister/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 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsRouter.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | @objc protocol ListGistsRoutingLogic 16 | { 17 | //func routeToSomewhere(segue: UIStoryboardSegue?) 18 | } 19 | 20 | protocol ListGistsDataPassing 21 | { 22 | var dataStore: ListGistsDataStore? { get } 23 | } 24 | 25 | class ListGistsRouter: NSObject, ListGistsRoutingLogic, ListGistsDataPassing 26 | { 27 | weak var viewController: ListGistsViewController? 28 | var dataStore: ListGistsDataStore? 29 | 30 | // MARK: Routing 31 | 32 | //func routeToSomewhere(segue: UIStoryboardSegue?) 33 | //{ 34 | // if let segue = segue { 35 | // let destinationVC = segue.destination as! SomewhereViewController 36 | // var destinationDS = destinationVC.router!.dataStore! 37 | // passDataToSomewhere(source: dataStore!, destination: &destinationDS) 38 | // } else { 39 | // let storyboard = UIStoryboard(name: "Main", bundle: nil) 40 | // let destinationVC = storyboard.instantiateViewController(withIdentifier: "SomewhereViewController") as! SomewhereViewController 41 | // var destinationDS = destinationVC.router!.dataStore! 42 | // passDataToSomewhere(source: dataStore!, destination: &destinationDS) 43 | // navigateToSomewhere(source: viewController!, destination: destinationVC) 44 | // } 45 | //} 46 | 47 | // MARK: Navigation 48 | 49 | //func navigateToSomewhere(source: ListGistsViewController, destination: SomewhereViewController) 50 | //{ 51 | // source.show(destination, sender: nil) 52 | //} 53 | 54 | // MARK: Passing data 55 | 56 | //func passDataToSomewhere(source: ListGistsDataStore, destination: inout SomewhereDataStore) 57 | //{ 58 | // destination.name = source.name 59 | //} 60 | } 61 | -------------------------------------------------------------------------------- /GisterTests/Scenes/ListGists/ListGistsPresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsPresenterTests.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | @testable import Gister 14 | import XCTest 15 | 16 | class ListGistsPresenterTests: XCTestCase 17 | { 18 | // MARK: Subject under test 19 | 20 | var sut: ListGistsPresenter! 21 | 22 | // MARK: Test lifecycle 23 | 24 | override func setUp() 25 | { 26 | super.setUp() 27 | setupListGistsPresenter() 28 | } 29 | 30 | override func tearDown() 31 | { 32 | super.tearDown() 33 | } 34 | 35 | // MARK: Test setup 36 | 37 | func setupListGistsPresenter() 38 | { 39 | sut = ListGistsPresenter() 40 | } 41 | 42 | // MARK: Test doubles 43 | 44 | class ListGistsDisplayLogicSpy: ListGistsDisplayLogic 45 | { 46 | var displayFetchedGistsCalled = false 47 | var displayFetchedGistsViewModel: ListGists.FetchGists.ViewModel! 48 | 49 | func displayFetchedGists(viewModel: ListGists.FetchGists.ViewModel) 50 | { 51 | displayFetchedGistsCalled = true 52 | displayFetchedGistsViewModel = viewModel 53 | } 54 | } 55 | 56 | // MARK: Tests 57 | 58 | func testPresentFetchedGistsShouldAskViewControllerToDisplayGists() 59 | { 60 | // Given 61 | let listGistsDisplayLogicSpy = ListGistsDisplayLogicSpy() 62 | sut.viewController = listGistsDisplayLogicSpy 63 | 64 | // When 65 | let gists = [Seeds.Gists.text, Seeds.Gists.html] 66 | let response = ListGists.FetchGists.Response(gists: gists) 67 | sut.presentFetchedGists(response: response) 68 | 69 | // Then 70 | let expectedGists = [Seeds.DisplayedGists.text, Seeds.DisplayedGists.html] 71 | let actualGists = listGistsDisplayLogicSpy.displayFetchedGistsViewModel.displayedGists 72 | XCTAssertTrue(listGistsDisplayLogicSpy.displayFetchedGistsCalled, "presentFetchedGists(response:) should ask the view controller to display gists") 73 | XCTAssertEqual(actualGists, expectedGists, "presentFetchedGists(response:) should display the correct gists") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Gister/Scenes/ListGists/ListGistsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsViewController.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | protocol ListGistsDisplayLogic: class 16 | { 17 | func displayFetchedGists(viewModel: ListGists.FetchGists.ViewModel) 18 | } 19 | 20 | class ListGistsViewController: UITableViewController, ListGistsDisplayLogic 21 | { 22 | var interactor: ListGistsBusinessLogic? 23 | var router: (NSObjectProtocol & ListGistsRoutingLogic & ListGistsDataPassing)? 24 | 25 | // MARK: Object lifecycle 26 | 27 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) 28 | { 29 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 30 | setup() 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) 34 | { 35 | super.init(coder: aDecoder) 36 | setup() 37 | } 38 | 39 | // MARK: Setup 40 | 41 | private func setup() 42 | { 43 | let viewController = self 44 | let interactor = ListGistsInteractor() 45 | let presenter = ListGistsPresenter() 46 | let router = ListGistsRouter() 47 | viewController.interactor = interactor 48 | viewController.router = router 49 | interactor.presenter = presenter 50 | presenter.viewController = viewController 51 | router.viewController = viewController 52 | router.dataStore = interactor 53 | } 54 | 55 | // MARK: Routing 56 | 57 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) 58 | { 59 | if let scene = segue.identifier { 60 | let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") 61 | if let router = router, router.responds(to: selector) { 62 | router.perform(selector, with: segue) 63 | } 64 | } 65 | } 66 | 67 | // MARK: View lifecycle 68 | 69 | override func viewDidLoad() 70 | { 71 | super.viewDidLoad() 72 | fetchGists() 73 | } 74 | 75 | // MARK: Table view 76 | 77 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 78 | { 79 | return displayedGists.count 80 | } 81 | 82 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 83 | { 84 | let displayedGist = displayedGists[indexPath.row] 85 | var cell = tableView.dequeueReusableCell(withIdentifier: "GistCell") 86 | if cell == nil { 87 | cell = UITableViewCell(style: .value1, reuseIdentifier: "GistCell") 88 | } 89 | cell?.textLabel?.text = displayedGist.login 90 | cell?.detailTextLabel?.text = displayedGist.filetype 91 | return cell! 92 | } 93 | 94 | // MARK: Fetch Gists 95 | 96 | var displayedGists: [ListGists.FetchGists.ViewModel.DisplayedGist] = [] 97 | 98 | @IBAction func refreshButtonTapped(_ sender: Any) 99 | { 100 | fetchGists() 101 | } 102 | 103 | func fetchGists() 104 | { 105 | let request = ListGists.FetchGists.Request() 106 | interactor?.fetchGists(request: request) 107 | } 108 | 109 | func displayFetchedGists(viewModel: ListGists.FetchGists.ViewModel) 110 | { 111 | displayedGists = viewModel.displayedGists 112 | tableView.reloadData() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /GisterTests/Scenes/ListGists/ListGistsViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsViewControllerTests.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | @testable import Gister 14 | import XCTest 15 | 16 | class ListGistsViewControllerTests: XCTestCase 17 | { 18 | // MARK: Subject under test 19 | 20 | var sut: ListGistsViewController! 21 | var window: UIWindow! 22 | 23 | // MARK: Test lifecycle 24 | 25 | override func setUp() 26 | { 27 | super.setUp() 28 | window = UIWindow() 29 | setupListGistsViewController() 30 | } 31 | 32 | override func tearDown() 33 | { 34 | window = nil 35 | super.tearDown() 36 | } 37 | 38 | // MARK: Test setup 39 | 40 | func setupListGistsViewController() 41 | { 42 | let bundle = Bundle.main 43 | let storyboard = UIStoryboard(name: "Main", bundle: bundle) 44 | sut = storyboard.instantiateViewController(withIdentifier: "ListGistsViewController") as! ListGistsViewController 45 | } 46 | 47 | func loadView() 48 | { 49 | window.addSubview(sut.view) 50 | RunLoop.current.run(until: Date()) 51 | } 52 | 53 | // MARK: Test doubles 54 | 55 | class TableViewSpy: UITableView 56 | { 57 | // MARK: Method call expectations 58 | 59 | var reloadDataCalled = false 60 | 61 | // MARK: Spied methods 62 | 63 | override func reloadData() 64 | { 65 | reloadDataCalled = true 66 | } 67 | } 68 | 69 | class ListGistsBusinessLogicSpy: ListGistsBusinessLogic 70 | { 71 | var fetchGistsCalled = false 72 | 73 | func fetchGists(request: ListGists.FetchGists.Request) 74 | { 75 | fetchGistsCalled = true 76 | } 77 | } 78 | 79 | // MARK: Tests 80 | 81 | func testShouldFetchGistsWhenViewIsLoaded() 82 | { 83 | // Given 84 | let listGistsBusinessLogicSpy = ListGistsBusinessLogicSpy() 85 | sut.interactor = listGistsBusinessLogicSpy 86 | 87 | // When 88 | loadView() 89 | 90 | // Then 91 | XCTAssertTrue(listGistsBusinessLogicSpy.fetchGistsCalled, "viewDidLoad() should ask the interactor to fetch gists") 92 | } 93 | 94 | func testShouldFetchGistsWhenRefreshButtonIsTapped() 95 | { 96 | // Given 97 | let listGistsBusinessLogicSpy = ListGistsBusinessLogicSpy() 98 | sut.interactor = listGistsBusinessLogicSpy 99 | loadView() 100 | 101 | // When 102 | sut.refreshButtonTapped(sut) 103 | 104 | // Then 105 | XCTAssertTrue(listGistsBusinessLogicSpy.fetchGistsCalled, "viewDidLoad() should ask the interactor to fetch gists") 106 | } 107 | 108 | func testDisplayFetchedGistsShouldReloadTableView() 109 | { 110 | // Given 111 | let tableViewSpy = TableViewSpy() 112 | sut.tableView = tableViewSpy 113 | loadView() 114 | 115 | // When 116 | let expectedGists = [Seeds.DisplayedGists.text, Seeds.DisplayedGists.html] 117 | let viewModel = ListGists.FetchGists.ViewModel(displayedGists: expectedGists) 118 | sut.displayFetchedGists(viewModel: viewModel) 119 | 120 | // Then 121 | let actualGists = sut.displayedGists 122 | XCTAssertEqual(actualGists, expectedGists, "displayFetchedGists(viewModel:) should display the gists results") 123 | XCTAssert(tableViewSpy.reloadDataCalled, "displayFetchedGists(viewModel:) should reload the table view") 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Gister/APIs/GistAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GistAPI.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol GistAPIProtocol 12 | { 13 | func fetch(completionHandler: @escaping ([Gist]) -> Void) 14 | func fetch() 15 | var delegate: GistAPIDelegate? { get set } 16 | } 17 | 18 | protocol GistAPIDelegate 19 | { 20 | func gistAPI(gistAPI: GistAPIProtocol, didFetchGists gists: [Gist]) 21 | } 22 | 23 | class GistAPI: NSObject, GistAPIProtocol, URLSessionDataDelegate, URLSessionTaskDelegate 24 | { 25 | var delegate: GistAPIDelegate? 26 | lazy var session: URLSession = { 27 | let config = URLSessionConfiguration.ephemeral 28 | if let delegate = self.delegate { 29 | return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main) 30 | } else { 31 | return URLSession(configuration: config) 32 | } 33 | }() 34 | private var dataTask: URLSessionDataTask! 35 | var results = [String: NSMutableData]() 36 | // Reference: https://developer.github.com/v3/gists 37 | private let url = URL(string: "https://api.github.com/gists/public")! 38 | 39 | // MARK: - Block implementation 40 | 41 | func fetch(completionHandler: @escaping ([Gist]) -> Void) 42 | { 43 | dataTask = session.dataTask(with: url) { (data, response, error) in 44 | if let data = data { 45 | let json = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) 46 | let gists = self.parseJSONToGists(json: json) 47 | DispatchQueue.main.async { 48 | completionHandler(gists) 49 | } 50 | } else { 51 | DispatchQueue.main.async { 52 | completionHandler([]) 53 | } 54 | } 55 | } 56 | dataTask.resume() 57 | } 58 | 59 | // MARK: - Delegate implementation 60 | 61 | func fetch() 62 | { 63 | dataTask = session.dataTask(with: url) 64 | dataTask.resume() 65 | } 66 | 67 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) 68 | { 69 | let key = String(dataTask.taskIdentifier) 70 | var result = results[key] 71 | if result == nil { 72 | result = NSMutableData(data: data) 73 | results[key] = result 74 | } else { 75 | result?.append(data) 76 | } 77 | } 78 | 79 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) 80 | { 81 | if let _ = error { 82 | delegate?.gistAPI(gistAPI: self, didFetchGists: []) 83 | } else { 84 | let key = String(task.taskIdentifier) 85 | if let result = results[key] as Data? { 86 | let json = try! JSONSerialization.jsonObject(with: result, options: .allowFragments) 87 | results[key] = nil 88 | let gists = self.parseJSONToGists(json: json) 89 | delegate?.gistAPI(gistAPI: self, didFetchGists: gists) 90 | } else { 91 | delegate?.gistAPI(gistAPI: self, didFetchGists: []) 92 | } 93 | } 94 | } 95 | 96 | // MARK: - Parsing Gist JSON 97 | 98 | private func parseJSONToGists(json: Any) -> [Gist] 99 | { 100 | var gists = [Gist]() 101 | 102 | if let array = json as? [Any] { 103 | for hash in array { 104 | 105 | var login: String? 106 | var url: String? 107 | var filename: String? 108 | var filetype: String? 109 | 110 | if let hash = hash as? [String: Any] { 111 | 112 | if let owner = hash["owner"] as? [String: Any] { 113 | login = owner["login"] as? String 114 | } else { 115 | login = "no user" 116 | } 117 | 118 | url = hash["html_url"] as? String 119 | 120 | if let files = hash["files"] as? [String: Any]{ 121 | if let file = files.first?.value as? [String: Any] { 122 | filename = file["filename"] as? String 123 | filetype = file["type"] as? String 124 | } 125 | } 126 | } 127 | 128 | if let login = login, let url = url, let filename = filename, let filetype = filetype { 129 | let gist = Gist(login: login, url: url, filename: filename, filetype: filetype) 130 | gists.append(gist) 131 | } 132 | } 133 | } 134 | 135 | return gists 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /GisterTests/Scenes/ListGists/ListGistsWorkerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsWorkerTests.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | @testable import Gister 14 | import XCTest 15 | 16 | class ListGistsWorkerTests: XCTestCase 17 | { 18 | // MARK: Subject under test 19 | 20 | var sut: ListGistsWorker! 21 | 22 | // MARK: Test lifecycle 23 | 24 | override func setUp() 25 | { 26 | super.setUp() 27 | setupListGistsWorker() 28 | } 29 | 30 | override func tearDown() 31 | { 32 | super.tearDown() 33 | } 34 | 35 | // MARK: Test setup 36 | 37 | func setupListGistsWorker() 38 | { 39 | sut = ListGistsWorker() 40 | } 41 | 42 | // MARK: Test doubles 43 | 44 | class GistAPISpy: GistAPIProtocol 45 | { 46 | let gists = [Seeds.Gists.text, Seeds.Gists.html] 47 | 48 | var fetchWithCompletionHandlerCalled = false 49 | var fetchWithDelegateCalled = false 50 | var delegate: GistAPIDelegate? 51 | 52 | func fetch(completionHandler: @escaping ([Gist]) -> Void) 53 | { 54 | fetchWithCompletionHandlerCalled = true 55 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 56 | completionHandler(self.gists) 57 | } 58 | } 59 | 60 | func fetch() 61 | { 62 | fetchWithDelegateCalled = true 63 | } 64 | } 65 | 66 | class ListGistsWorkerDelegateSpy: ListGistsWorkerDelegate 67 | { 68 | var listGistsWorkerDidFetchGistsCalled = false 69 | var listGistsWorkerDidFetchGistsResults = [Gist]() 70 | 71 | func listGistsWorker(listGistsWorker: ListGistsWorker, didFetchGists gists: [Gist]) 72 | { 73 | listGistsWorkerDidFetchGistsCalled = true 74 | listGistsWorkerDidFetchGistsResults = gists 75 | } 76 | } 77 | 78 | // MARK: Tests 79 | 80 | // MARK: Block implementation 81 | 82 | func testFetchShouldAskGistAPIToFetchGistsWithBlock() 83 | { 84 | // Given 85 | let gistAPISpy = GistAPISpy() 86 | sut.gistAPI = gistAPISpy 87 | 88 | // When 89 | sut.fetch { (gists) in } 90 | 91 | // Then 92 | XCTAssertTrue(gistAPISpy.fetchWithCompletionHandlerCalled, "fetch(completionHandler:) should ask Gist API to fetch gists") 93 | } 94 | 95 | func testFetchShouldReturnGistsResultsToBlock() 96 | { 97 | // Given 98 | let gistAPISpy = GistAPISpy() 99 | sut.gistAPI = gistAPISpy 100 | 101 | // When 102 | var actualGists: [Gist]? 103 | let fetchCompleted = expectation(description: "Wait for fetch to complete") 104 | sut.fetch { (gists) in 105 | actualGists = gists 106 | fetchCompleted.fulfill() 107 | } 108 | waitForExpectations(timeout: 5.0, handler: nil) 109 | 110 | // Then 111 | let expectedGists = gistAPISpy.gists 112 | XCTAssertEqual(actualGists!, expectedGists, "fetch(completionHandler:) should return an array of gists to completion block if the fetch succeeds") 113 | } 114 | 115 | // MARK: Delegate implementation 116 | 117 | func testFetchShouldAskGistAPIToFetchGistsWithDelegate() 118 | { 119 | // Given 120 | let gistAPISpy = GistAPISpy() 121 | sut.gistAPI = gistAPISpy 122 | 123 | // When 124 | sut.fetch() 125 | 126 | // Then 127 | XCTAssertTrue(gistAPISpy.fetchWithDelegateCalled, "fetch(completionHandler:) should ask Gist API to fetch gists") 128 | } 129 | 130 | func testGistAPIDidFetchGistsShouldNotifyDelegateWithGistsResults() 131 | { 132 | // Given 133 | let gistAPISpy = GistAPISpy() 134 | sut.gistAPI = gistAPISpy 135 | let listGistsWorkerDelegateSpy = ListGistsWorkerDelegateSpy() 136 | sut.delegate = listGistsWorkerDelegateSpy 137 | 138 | // When 139 | let gists = [Seeds.Gists.text, Seeds.Gists.html] 140 | sut.gistAPI(gistAPI: gistAPISpy, didFetchGists: gists) 141 | 142 | // Then 143 | let expectedGists = gistAPISpy.gists 144 | let actualGists = listGistsWorkerDelegateSpy.listGistsWorkerDidFetchGistsResults 145 | XCTAssertTrue(listGistsWorkerDelegateSpy.listGistsWorkerDidFetchGistsCalled, "fetch(completionHandler:) should notify its delegate") 146 | XCTAssertEqual(actualGists, expectedGists, "fetch(completionHandler:) should return an array of gists if the fetch succeeds") 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Gister/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate 14 | { 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 18 | { 19 | // Override point for customization after application launch. 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) 24 | { 25 | // 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. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | func applicationDidEnterBackground(_ application: UIApplication) 30 | { 31 | // 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. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(_ application: UIApplication) 36 | { 37 | // 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. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) 41 | { 42 | // 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. 43 | } 44 | 45 | func applicationWillTerminate(_ application: UIApplication) 46 | { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | // Saves changes in the application's managed object context before the application terminates. 49 | self.saveContext() 50 | } 51 | 52 | // MARK: - Core Data stack 53 | 54 | lazy var persistentContainer: NSPersistentContainer = { 55 | /* 56 | The persistent container for the application. This implementation 57 | creates and returns a container, having loaded the store for the 58 | application to it. This property is optional since there are legitimate 59 | error conditions that could cause the creation of the store to fail. 60 | */ 61 | let container = NSPersistentContainer(name: "Gister") 62 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 63 | if let error = error as NSError? { 64 | // Replace this implementation with code to handle the error appropriately. 65 | // 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. 66 | 67 | /* 68 | Typical reasons for an error here include: 69 | * The parent directory does not exist, cannot be created, or disallows writing. 70 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 71 | * The device is out of space. 72 | * The store could not be migrated to the current model version. 73 | Check the error message to determine what the actual problem was. 74 | */ 75 | fatalError("Unresolved error \(error), \(error.userInfo)") 76 | } 77 | }) 78 | return container 79 | }() 80 | 81 | // MARK: - Core Data Saving support 82 | 83 | func saveContext () 84 | { 85 | let context = persistentContainer.viewContext 86 | if context.hasChanges { 87 | do { 88 | try context.save() 89 | } catch { 90 | // Replace this implementation with code to handle the error appropriately. 91 | // 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. 92 | let nserror = error as NSError 93 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /GisterTests/Scenes/ListGists/ListGistsInteractorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListGistsInteractorTests.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright (c) 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | @testable import Gister 14 | import XCTest 15 | 16 | class ListGistsInteractorTests: XCTestCase 17 | { 18 | // MARK: Subject under test 19 | 20 | var sut: ListGistsInteractor! 21 | 22 | // MARK: Test lifecycle 23 | 24 | override func setUp() 25 | { 26 | super.setUp() 27 | setupListGistsInteractor() 28 | } 29 | 30 | override func tearDown() 31 | { 32 | super.tearDown() 33 | } 34 | 35 | // MARK: Test setup 36 | 37 | func setupListGistsInteractor() 38 | { 39 | sut = ListGistsInteractor() 40 | } 41 | 42 | // MARK: Test doubles 43 | 44 | class ListGistsWorkerSpy: ListGistsWorker 45 | { 46 | let gists = [Seeds.Gists.text, Seeds.Gists.html] 47 | 48 | var fetchWithCompletionHandlerCalled = false 49 | var fetchWithDelegateCalled = false 50 | 51 | override func fetch(completionHandler: @escaping ([Gist]) -> Void) 52 | { 53 | fetchWithCompletionHandlerCalled = true 54 | completionHandler(gists) 55 | } 56 | 57 | override func fetch() 58 | { 59 | fetchWithDelegateCalled = true 60 | delegate?.listGistsWorker(listGistsWorker: self, didFetchGists: gists) 61 | } 62 | } 63 | 64 | class ListGistsPresentationLogicSpy: ListGistsPresentationLogic 65 | { 66 | var presentFetchedGistsCalled = false 67 | 68 | func presentFetchedGists(response: ListGists.FetchGists.Response) 69 | { 70 | presentFetchedGistsCalled = true 71 | } 72 | } 73 | 74 | // MARK: Tests 75 | 76 | // MARK: Block implementation 77 | 78 | func testFetchGistsShouldAskWorkerToFetchGistsWithBlock() 79 | { 80 | guard sut.asyncOpKind == .block else { return } 81 | 82 | // Given 83 | let listGistsWorkerSpy = ListGistsWorkerSpy() 84 | sut.listGistsWorker = listGistsWorkerSpy 85 | 86 | // When 87 | let request = ListGists.FetchGists.Request() 88 | sut.fetchGists(request: request) 89 | 90 | // Then 91 | XCTAssertTrue(listGistsWorkerSpy.fetchWithCompletionHandlerCalled, "fetchGists(request:) should ask the worker to fetch gists") 92 | } 93 | 94 | func testFetchGistsShouldAskPresenterToFormatGists() 95 | { 96 | // Given 97 | let listGistsWorkerSpy = ListGistsWorkerSpy() 98 | sut.listGistsWorker = listGistsWorkerSpy 99 | let listGistsPresentationLogicSpy = ListGistsPresentationLogicSpy() 100 | sut.presenter = listGistsPresentationLogicSpy 101 | 102 | // When 103 | let request = ListGists.FetchGists.Request() 104 | sut.fetchGists(request: request) 105 | 106 | // Then 107 | XCTAssertTrue(listGistsPresentationLogicSpy.presentFetchedGistsCalled, "fetchGists(request:) should ask the presenter to format gists") 108 | } 109 | 110 | // MARK: Delegate implementation 111 | 112 | func testFetchGistsShouldAskWorkerToFetchGistsWithDelegate() 113 | { 114 | guard sut.asyncOpKind == .delegate else { return } 115 | 116 | // Given 117 | let listGistsWorkerSpy = ListGistsWorkerSpy() 118 | sut.listGistsWorker = listGistsWorkerSpy 119 | 120 | // When 121 | let request = ListGists.FetchGists.Request() 122 | sut.fetchGists(request: request) 123 | 124 | // Then 125 | XCTAssertTrue(listGistsWorkerSpy.fetchWithDelegateCalled, "fetchGists(request:) should ask the worker to fetch gists") 126 | XCTAssertNotNil(sut.listGistsWorker.delegate, "fetchGists(request:) should set itself to be the delegate to be notified of fetch results") 127 | } 128 | 129 | func testListGistsWorkerDidFetchGistsShouldAskPresenterToFormatGists() 130 | { 131 | // Given 132 | let listGistsWorkerSpy = ListGistsWorkerSpy() 133 | sut.listGistsWorker = listGistsWorkerSpy 134 | let listGistsPresentationLogicSpy = ListGistsPresentationLogicSpy() 135 | sut.presenter = listGistsPresentationLogicSpy 136 | 137 | // When 138 | sut.listGistsWorker(listGistsWorker: listGistsWorkerSpy, didFetchGists: listGistsWorkerSpy.gists) 139 | 140 | // Then 141 | XCTAssertTrue(listGistsPresentationLogicSpy.presentFetchedGistsCalled, "listGistsWorker(listGistsWorker:didFetchGists:) should ask the presenter to format gists") 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Gister/Base.lproj/Main.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 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /GisterTests/APIs/GistAPITest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GistAPITest.swift 3 | // Gister 4 | // 5 | // Created by Raymond Law on 10/12/17. 6 | // Copyright © 2017 Clean Swift LLC. All rights reserved. 7 | // 8 | 9 | @testable import Gister 10 | import XCTest 11 | 12 | // MARK: Test doubles 13 | 14 | class URLSessionSpy: URLSession 15 | { 16 | var dataTaskWithURLCalled = false 17 | var dataTask = URLSessionDataTaskSpy() 18 | var data: Data? 19 | 20 | override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask 21 | { 22 | dataTaskWithURLCalled = true 23 | completionHandler(data, nil, nil) 24 | return dataTask 25 | } 26 | 27 | override func dataTask(with url: URL) -> URLSessionDataTask 28 | { 29 | dataTaskWithURLCalled = true 30 | return dataTask 31 | } 32 | } 33 | 34 | class URLSessionDataTaskSpy: URLSessionDataTask 35 | { 36 | var resumeCalled = false 37 | override var taskIdentifier: Int { return 0 } 38 | 39 | override func resume() 40 | { 41 | resumeCalled = true 42 | } 43 | } 44 | 45 | class GistAPIDelegateSpy: GistAPIDelegate 46 | { 47 | var gistAPIDidFetchGistsCalled = false 48 | var gistAPIDidFetchGistsResults: [Gist]? 49 | 50 | func gistAPI(gistAPI: GistAPIProtocol, didFetchGists gists: [Gist]) 51 | { 52 | gistAPIDidFetchGistsCalled = true 53 | gistAPIDidFetchGistsResults = gists 54 | } 55 | } 56 | 57 | class GistAPITest: XCTestCase 58 | { 59 | // MARK: Subject under test 60 | 61 | var sut: GistAPI! 62 | 63 | // MARK: Test lifecycle 64 | 65 | override func setUp() 66 | { 67 | super.setUp() 68 | setupGistAPI() 69 | } 70 | 71 | override func tearDown() 72 | { 73 | super.tearDown() 74 | } 75 | 76 | // MARK: Test setup 77 | 78 | func setupGistAPI() 79 | { 80 | sut = GistAPI() 81 | } 82 | 83 | // MARK: Tests 84 | 85 | // MARK: - Block implementation 86 | 87 | func testFetchShouldAskURLSessionToFetchGistsFromGitHubWithBlock() 88 | { 89 | // Given 90 | let sessionSpy = URLSessionSpy() 91 | sut.session = sessionSpy 92 | 93 | // When 94 | let fetchCompleted = expectation(description: "Wait for fetch to complete") 95 | sut.fetch { (gists) in 96 | fetchCompleted.fulfill() 97 | } 98 | waitForExpectations(timeout: 5.0, handler: nil) 99 | 100 | // Then 101 | XCTAssertTrue(sessionSpy.dataTaskWithURLCalled, "fetch(completionHandler:) should ask URLSession to fetch gists from GitHub") 102 | XCTAssertTrue(sessionSpy.dataTask.resumeCalled, "fetch(completionHandler:) should start the data task") 103 | } 104 | 105 | func testFetchShouldParseJSONToGists() 106 | { 107 | // Given 108 | let sessionSpy = URLSessionSpy() 109 | sut.session = sessionSpy 110 | sessionSpy.data = Seeds.JSON.data 111 | 112 | // When 113 | var actualGists: [Gist]? 114 | let fetchCompleted = expectation(description: "Wait for fetch to complete") 115 | sut.fetch { (gists) in 116 | actualGists = gists 117 | fetchCompleted.fulfill() 118 | } 119 | waitForExpectations(timeout: 5.0, handler: nil) 120 | 121 | // Then 122 | let expectedGists = [Seeds.Gists.text, Seeds.Gists.html] 123 | XCTAssertEqual(actualGists!, expectedGists, "fetch(completionHandler:) should return an array of gists if the fetch succeeds") 124 | } 125 | 126 | func testFetchShouldFailGracefully() 127 | { 128 | // Given 129 | let sessionSpy = URLSessionSpy() 130 | sut.session = sessionSpy 131 | sessionSpy.data = nil 132 | 133 | // When 134 | var actualGists: [Gist]? 135 | let fetchCompleted = expectation(description: "Wait for fetch to complete") 136 | sut.fetch { (gists) in 137 | actualGists = gists 138 | fetchCompleted.fulfill() 139 | } 140 | waitForExpectations(timeout: 5.0, handler: nil) 141 | 142 | // Then 143 | let expectedGists = [Gist]() 144 | XCTAssertEqual(actualGists!, expectedGists, "fetch(completionHandler:) should return an empty array if the fetch fails") 145 | } 146 | 147 | // MARK: - Delegate implementation 148 | 149 | func testFetchShouldAskURLSessionToFetchGistsFromGitHubWithDelegate() 150 | { 151 | // Given 152 | let sessionSpy = URLSessionSpy() 153 | sut.session = sessionSpy 154 | 155 | // When 156 | sut.fetch() 157 | 158 | // Then 159 | XCTAssertTrue(sessionSpy.dataTaskWithURLCalled, "fetch(completionHandler:) should ask URLSession to fetch gists from GitHub") 160 | XCTAssertTrue(sessionSpy.dataTask.resumeCalled, "fetch(completionHandler:) should start the data task") 161 | } 162 | 163 | func testURLSessionTaskDidCompleteWithErrorShouldNotifyDelegateWithGistsResults() 164 | { 165 | // Given 166 | let sessionSpy = URLSessionSpy() 167 | sut.session = sessionSpy 168 | let gistAPIDelegateSpy = GistAPIDelegateSpy() 169 | sut.delegate = gistAPIDelegateSpy 170 | let dataTaskSpy = URLSessionDataTaskSpy() 171 | let key = String(dataTaskSpy.taskIdentifier) 172 | sut.results = [key: NSMutableData(data: Seeds.JSON.data)] 173 | 174 | // When 175 | sut.urlSession(sessionSpy, task: dataTaskSpy, didCompleteWithError: nil) 176 | 177 | // Then 178 | let actualGists = gistAPIDelegateSpy.gistAPIDidFetchGistsResults! 179 | let expectedGists = [Seeds.Gists.text, Seeds.Gists.html] 180 | XCTAssertTrue(gistAPIDelegateSpy.gistAPIDidFetchGistsCalled, "urlSession(_:task:didCompleteWithError:) should notify the delegate") 181 | XCTAssertEqual(actualGists, expectedGists, "urlSession(_:task:didCompleteWithError:) should return the correct gists results") 182 | } 183 | 184 | func testURLSessionTaskDidCompleteWithErrorShouldFailGracefully() 185 | { 186 | // Given 187 | let sessionSpy = URLSessionSpy() 188 | sut.session = sessionSpy 189 | let gistAPIDelegateSpy = GistAPIDelegateSpy() 190 | sut.delegate = gistAPIDelegateSpy 191 | let dataTaskSpy = URLSessionDataTaskSpy() 192 | let error: Error? = NSError(domain: "GisterAPI", code: 911, userInfo: ["Unit Test" : "No harm done."]) 193 | 194 | // When 195 | sut.urlSession(sessionSpy, task: dataTaskSpy, didCompleteWithError: error) 196 | 197 | // Then 198 | let actualGists = gistAPIDelegateSpy.gistAPIDidFetchGistsResults! 199 | XCTAssertTrue(gistAPIDelegateSpy.gistAPIDidFetchGistsCalled, "urlSession(_:task:didCompleteWithError:) should notify the delegate") 200 | XCTAssertTrue(actualGists.isEmpty, "urlSession(_:task:didCompleteWithError:) should return empty results") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Gister.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7160C7561F8FA4F300506FE1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7551F8FA4F300506FE1 /* AppDelegate.swift */; }; 11 | 7160C75B1F8FA4F300506FE1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7160C7591F8FA4F300506FE1 /* Main.storyboard */; }; 12 | 7160C75E1F8FA4F300506FE1 /* Gister.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7160C75C1F8FA4F300506FE1 /* Gister.xcdatamodeld */; }; 13 | 7160C7601F8FA4F300506FE1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7160C75F1F8FA4F300506FE1 /* Assets.xcassets */; }; 14 | 7160C7631F8FA4F300506FE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7160C7611F8FA4F300506FE1 /* LaunchScreen.storyboard */; }; 15 | 7160C76E1F8FA4F300506FE1 /* GisterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C76D1F8FA4F300506FE1 /* GisterTests.swift */; }; 16 | 7160C7791F8FA4F300506FE1 /* GisterUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7781F8FA4F300506FE1 /* GisterUITests.swift */; }; 17 | 7160C78E1F8FA62800506FE1 /* ListGistsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7881F8FA62800506FE1 /* ListGistsInteractor.swift */; }; 18 | 7160C78F1F8FA62800506FE1 /* ListGistsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7891F8FA62800506FE1 /* ListGistsModels.swift */; }; 19 | 7160C7901F8FA62800506FE1 /* ListGistsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C78A1F8FA62800506FE1 /* ListGistsPresenter.swift */; }; 20 | 7160C7911F8FA62800506FE1 /* ListGistsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C78B1F8FA62800506FE1 /* ListGistsRouter.swift */; }; 21 | 7160C7921F8FA62800506FE1 /* ListGistsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C78C1F8FA62800506FE1 /* ListGistsViewController.swift */; }; 22 | 7160C7931F8FA62800506FE1 /* ListGistsWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C78D1F8FA62800506FE1 /* ListGistsWorker.swift */; }; 23 | 7160C79A1F8FA68800506FE1 /* ListGistsInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7961F8FA68800506FE1 /* ListGistsInteractorTests.swift */; }; 24 | 7160C79B1F8FA68800506FE1 /* ListGistsPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7971F8FA68800506FE1 /* ListGistsPresenterTests.swift */; }; 25 | 7160C79C1F8FA68800506FE1 /* ListGistsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7981F8FA68800506FE1 /* ListGistsViewControllerTests.swift */; }; 26 | 7160C79D1F8FA68800506FE1 /* ListGistsWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7991F8FA68800506FE1 /* ListGistsWorkerTests.swift */; }; 27 | 7160C7A01F8FA73600506FE1 /* GistAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C79F1F8FA73600506FE1 /* GistAPI.swift */; }; 28 | 7160C7A51F90017D00506FE1 /* GistAPITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7160C7A41F90017D00506FE1 /* GistAPITest.swift */; }; 29 | 71C590591F9B08A50098542B /* Gist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C590581F9B08A50098542B /* Gist.swift */; }; 30 | 71D5A6BE1F9AC1B500BFA7A7 /* Gist.json in Resources */ = {isa = PBXBuildFile; fileRef = 71D5A6BD1F9AC1B500BFA7A7 /* Gist.json */; }; 31 | 71D5A6C01F9AC1F500BFA7A7 /* Seeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D5A6BF1F9AC1F500BFA7A7 /* Seeds.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 7160C76A1F8FA4F300506FE1 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 7160C74A1F8FA4F300506FE1 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 7160C7511F8FA4F300506FE1; 40 | remoteInfo = Gister; 41 | }; 42 | 7160C7751F8FA4F300506FE1 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 7160C74A1F8FA4F300506FE1 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 7160C7511F8FA4F300506FE1; 47 | remoteInfo = Gister; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 7160C7521F8FA4F300506FE1 /* Gister.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gister.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 7160C7551F8FA4F300506FE1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 7160C75A1F8FA4F300506FE1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | 7160C75D1F8FA4F300506FE1 /* Gister.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Gister.xcdatamodel; sourceTree = ""; }; 56 | 7160C75F1F8FA4F300506FE1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 7160C7621F8FA4F300506FE1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 7160C7641F8FA4F300506FE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 7160C7691F8FA4F300506FE1 /* GisterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GisterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 7160C76D1F8FA4F300506FE1 /* GisterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GisterTests.swift; sourceTree = ""; }; 61 | 7160C76F1F8FA4F300506FE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | 7160C7741F8FA4F300506FE1 /* GisterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GisterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 7160C7781F8FA4F300506FE1 /* GisterUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GisterUITests.swift; sourceTree = ""; }; 64 | 7160C77A1F8FA4F300506FE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | 7160C7881F8FA62800506FE1 /* ListGistsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsInteractor.swift; path = Scenes/ListGists/ListGistsInteractor.swift; sourceTree = ""; }; 66 | 7160C7891F8FA62800506FE1 /* ListGistsModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsModels.swift; path = Scenes/ListGists/ListGistsModels.swift; sourceTree = ""; }; 67 | 7160C78A1F8FA62800506FE1 /* ListGistsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsPresenter.swift; path = Scenes/ListGists/ListGistsPresenter.swift; sourceTree = ""; }; 68 | 7160C78B1F8FA62800506FE1 /* ListGistsRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsRouter.swift; path = Scenes/ListGists/ListGistsRouter.swift; sourceTree = ""; }; 69 | 7160C78C1F8FA62800506FE1 /* ListGistsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsViewController.swift; path = Scenes/ListGists/ListGistsViewController.swift; sourceTree = ""; }; 70 | 7160C78D1F8FA62800506FE1 /* ListGistsWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsWorker.swift; path = Scenes/ListGists/ListGistsWorker.swift; sourceTree = ""; }; 71 | 7160C7961F8FA68800506FE1 /* ListGistsInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsInteractorTests.swift; path = Scenes/ListGists/ListGistsInteractorTests.swift; sourceTree = ""; }; 72 | 7160C7971F8FA68800506FE1 /* ListGistsPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsPresenterTests.swift; path = Scenes/ListGists/ListGistsPresenterTests.swift; sourceTree = ""; }; 73 | 7160C7981F8FA68800506FE1 /* ListGistsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsViewControllerTests.swift; path = Scenes/ListGists/ListGistsViewControllerTests.swift; sourceTree = ""; }; 74 | 7160C7991F8FA68800506FE1 /* ListGistsWorkerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListGistsWorkerTests.swift; path = Scenes/ListGists/ListGistsWorkerTests.swift; sourceTree = ""; }; 75 | 7160C79F1F8FA73600506FE1 /* GistAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GistAPI.swift; path = APIs/GistAPI.swift; sourceTree = ""; }; 76 | 7160C7A41F90017D00506FE1 /* GistAPITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GistAPITest.swift; path = APIs/GistAPITest.swift; sourceTree = ""; }; 77 | 71C590581F9B08A50098542B /* Gist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Gist.swift; path = APIs/Gist.swift; sourceTree = ""; }; 78 | 71D5A6BD1F9AC1B500BFA7A7 /* Gist.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Gist.json; sourceTree = ""; }; 79 | 71D5A6BF1F9AC1F500BFA7A7 /* Seeds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Seeds.swift; sourceTree = ""; }; 80 | /* End PBXFileReference section */ 81 | 82 | /* Begin PBXFrameworksBuildPhase section */ 83 | 7160C74F1F8FA4F300506FE1 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | 7160C7661F8FA4F300506FE1 /* Frameworks */ = { 91 | isa = PBXFrameworksBuildPhase; 92 | buildActionMask = 2147483647; 93 | files = ( 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 7160C7711F8FA4F300506FE1 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | /* End PBXFrameworksBuildPhase section */ 105 | 106 | /* Begin PBXGroup section */ 107 | 7160C7491F8FA4F300506FE1 = { 108 | isa = PBXGroup; 109 | children = ( 110 | 7160C7541F8FA4F300506FE1 /* Gister */, 111 | 7160C76C1F8FA4F300506FE1 /* GisterTests */, 112 | 7160C7771F8FA4F300506FE1 /* GisterUITests */, 113 | 7160C7531F8FA4F300506FE1 /* Products */, 114 | ); 115 | sourceTree = ""; 116 | }; 117 | 7160C7531F8FA4F300506FE1 /* Products */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 7160C7521F8FA4F300506FE1 /* Gister.app */, 121 | 7160C7691F8FA4F300506FE1 /* GisterTests.xctest */, 122 | 7160C7741F8FA4F300506FE1 /* GisterUITests.xctest */, 123 | ); 124 | name = Products; 125 | sourceTree = ""; 126 | }; 127 | 7160C7541F8FA4F300506FE1 /* Gister */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 7160C79E1F8FA6EA00506FE1 /* APIs */, 131 | 7160C7861F8FA60400506FE1 /* Scenes */, 132 | 7160C7551F8FA4F300506FE1 /* AppDelegate.swift */, 133 | 7160C7591F8FA4F300506FE1 /* Main.storyboard */, 134 | 7160C75F1F8FA4F300506FE1 /* Assets.xcassets */, 135 | 7160C7611F8FA4F300506FE1 /* LaunchScreen.storyboard */, 136 | 7160C7641F8FA4F300506FE1 /* Info.plist */, 137 | 7160C75C1F8FA4F300506FE1 /* Gister.xcdatamodeld */, 138 | ); 139 | path = Gister; 140 | sourceTree = ""; 141 | }; 142 | 7160C76C1F8FA4F300506FE1 /* GisterTests */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 7160C7A11F90014600506FE1 /* APIs */, 146 | 7160C7941F8FA66D00506FE1 /* Scenes */, 147 | 7160C76D1F8FA4F300506FE1 /* GisterTests.swift */, 148 | 71D5A6BF1F9AC1F500BFA7A7 /* Seeds.swift */, 149 | 71D5A6BD1F9AC1B500BFA7A7 /* Gist.json */, 150 | 7160C76F1F8FA4F300506FE1 /* Info.plist */, 151 | ); 152 | path = GisterTests; 153 | sourceTree = ""; 154 | }; 155 | 7160C7771F8FA4F300506FE1 /* GisterUITests */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 7160C7781F8FA4F300506FE1 /* GisterUITests.swift */, 159 | 7160C77A1F8FA4F300506FE1 /* Info.plist */, 160 | ); 161 | path = GisterUITests; 162 | sourceTree = ""; 163 | }; 164 | 7160C7861F8FA60400506FE1 /* Scenes */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 7160C7871F8FA60600506FE1 /* ListGists */, 168 | ); 169 | name = Scenes; 170 | sourceTree = ""; 171 | }; 172 | 7160C7871F8FA60600506FE1 /* ListGists */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 7160C7881F8FA62800506FE1 /* ListGistsInteractor.swift */, 176 | 7160C7891F8FA62800506FE1 /* ListGistsModels.swift */, 177 | 7160C78A1F8FA62800506FE1 /* ListGistsPresenter.swift */, 178 | 7160C78B1F8FA62800506FE1 /* ListGistsRouter.swift */, 179 | 7160C78C1F8FA62800506FE1 /* ListGistsViewController.swift */, 180 | 7160C78D1F8FA62800506FE1 /* ListGistsWorker.swift */, 181 | ); 182 | name = ListGists; 183 | sourceTree = ""; 184 | }; 185 | 7160C7941F8FA66D00506FE1 /* Scenes */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 7160C7951F8FA66D00506FE1 /* ListGists */, 189 | ); 190 | name = Scenes; 191 | sourceTree = ""; 192 | }; 193 | 7160C7951F8FA66D00506FE1 /* ListGists */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 7160C7961F8FA68800506FE1 /* ListGistsInteractorTests.swift */, 197 | 7160C7971F8FA68800506FE1 /* ListGistsPresenterTests.swift */, 198 | 7160C7981F8FA68800506FE1 /* ListGistsViewControllerTests.swift */, 199 | 7160C7991F8FA68800506FE1 /* ListGistsWorkerTests.swift */, 200 | ); 201 | name = ListGists; 202 | sourceTree = ""; 203 | }; 204 | 7160C79E1F8FA6EA00506FE1 /* APIs */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 7160C79F1F8FA73600506FE1 /* GistAPI.swift */, 208 | 71C590581F9B08A50098542B /* Gist.swift */, 209 | ); 210 | name = APIs; 211 | sourceTree = ""; 212 | }; 213 | 7160C7A11F90014600506FE1 /* APIs */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 7160C7A41F90017D00506FE1 /* GistAPITest.swift */, 217 | ); 218 | name = APIs; 219 | sourceTree = ""; 220 | }; 221 | /* End PBXGroup section */ 222 | 223 | /* Begin PBXNativeTarget section */ 224 | 7160C7511F8FA4F300506FE1 /* Gister */ = { 225 | isa = PBXNativeTarget; 226 | buildConfigurationList = 7160C77D1F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "Gister" */; 227 | buildPhases = ( 228 | 7160C74E1F8FA4F300506FE1 /* Sources */, 229 | 7160C74F1F8FA4F300506FE1 /* Frameworks */, 230 | 7160C7501F8FA4F300506FE1 /* Resources */, 231 | ); 232 | buildRules = ( 233 | ); 234 | dependencies = ( 235 | ); 236 | name = Gister; 237 | productName = Gister; 238 | productReference = 7160C7521F8FA4F300506FE1 /* Gister.app */; 239 | productType = "com.apple.product-type.application"; 240 | }; 241 | 7160C7681F8FA4F300506FE1 /* GisterTests */ = { 242 | isa = PBXNativeTarget; 243 | buildConfigurationList = 7160C7801F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "GisterTests" */; 244 | buildPhases = ( 245 | 7160C7651F8FA4F300506FE1 /* Sources */, 246 | 7160C7661F8FA4F300506FE1 /* Frameworks */, 247 | 7160C7671F8FA4F300506FE1 /* Resources */, 248 | ); 249 | buildRules = ( 250 | ); 251 | dependencies = ( 252 | 7160C76B1F8FA4F300506FE1 /* PBXTargetDependency */, 253 | ); 254 | name = GisterTests; 255 | productName = GisterTests; 256 | productReference = 7160C7691F8FA4F300506FE1 /* GisterTests.xctest */; 257 | productType = "com.apple.product-type.bundle.unit-test"; 258 | }; 259 | 7160C7731F8FA4F300506FE1 /* GisterUITests */ = { 260 | isa = PBXNativeTarget; 261 | buildConfigurationList = 7160C7831F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "GisterUITests" */; 262 | buildPhases = ( 263 | 7160C7701F8FA4F300506FE1 /* Sources */, 264 | 7160C7711F8FA4F300506FE1 /* Frameworks */, 265 | 7160C7721F8FA4F300506FE1 /* Resources */, 266 | ); 267 | buildRules = ( 268 | ); 269 | dependencies = ( 270 | 7160C7761F8FA4F300506FE1 /* PBXTargetDependency */, 271 | ); 272 | name = GisterUITests; 273 | productName = GisterUITests; 274 | productReference = 7160C7741F8FA4F300506FE1 /* GisterUITests.xctest */; 275 | productType = "com.apple.product-type.bundle.ui-testing"; 276 | }; 277 | /* End PBXNativeTarget section */ 278 | 279 | /* Begin PBXProject section */ 280 | 7160C74A1F8FA4F300506FE1 /* Project object */ = { 281 | isa = PBXProject; 282 | attributes = { 283 | LastSwiftUpdateCheck = 0830; 284 | LastUpgradeCheck = 0930; 285 | ORGANIZATIONNAME = "Clean Swift LLC"; 286 | TargetAttributes = { 287 | 7160C7511F8FA4F300506FE1 = { 288 | CreatedOnToolsVersion = 8.3.3; 289 | DevelopmentTeam = 5LBZDN8XB8; 290 | LastSwiftMigration = 0930; 291 | ProvisioningStyle = Automatic; 292 | }; 293 | 7160C7681F8FA4F300506FE1 = { 294 | CreatedOnToolsVersion = 8.3.3; 295 | DevelopmentTeam = 5LBZDN8XB8; 296 | LastSwiftMigration = 0930; 297 | ProvisioningStyle = Automatic; 298 | TestTargetID = 7160C7511F8FA4F300506FE1; 299 | }; 300 | 7160C7731F8FA4F300506FE1 = { 301 | CreatedOnToolsVersion = 8.3.3; 302 | DevelopmentTeam = 5LBZDN8XB8; 303 | LastSwiftMigration = 0930; 304 | ProvisioningStyle = Automatic; 305 | TestTargetID = 7160C7511F8FA4F300506FE1; 306 | }; 307 | }; 308 | }; 309 | buildConfigurationList = 7160C74D1F8FA4F300506FE1 /* Build configuration list for PBXProject "Gister" */; 310 | compatibilityVersion = "Xcode 3.2"; 311 | developmentRegion = English; 312 | hasScannedForEncodings = 0; 313 | knownRegions = ( 314 | en, 315 | Base, 316 | ); 317 | mainGroup = 7160C7491F8FA4F300506FE1; 318 | productRefGroup = 7160C7531F8FA4F300506FE1 /* Products */; 319 | projectDirPath = ""; 320 | projectRoot = ""; 321 | targets = ( 322 | 7160C7511F8FA4F300506FE1 /* Gister */, 323 | 7160C7681F8FA4F300506FE1 /* GisterTests */, 324 | 7160C7731F8FA4F300506FE1 /* GisterUITests */, 325 | ); 326 | }; 327 | /* End PBXProject section */ 328 | 329 | /* Begin PBXResourcesBuildPhase section */ 330 | 7160C7501F8FA4F300506FE1 /* Resources */ = { 331 | isa = PBXResourcesBuildPhase; 332 | buildActionMask = 2147483647; 333 | files = ( 334 | 7160C7631F8FA4F300506FE1 /* LaunchScreen.storyboard in Resources */, 335 | 7160C7601F8FA4F300506FE1 /* Assets.xcassets in Resources */, 336 | 7160C75B1F8FA4F300506FE1 /* Main.storyboard in Resources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | 7160C7671F8FA4F300506FE1 /* Resources */ = { 341 | isa = PBXResourcesBuildPhase; 342 | buildActionMask = 2147483647; 343 | files = ( 344 | 71D5A6BE1F9AC1B500BFA7A7 /* Gist.json in Resources */, 345 | ); 346 | runOnlyForDeploymentPostprocessing = 0; 347 | }; 348 | 7160C7721F8FA4F300506FE1 /* Resources */ = { 349 | isa = PBXResourcesBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | /* End PBXResourcesBuildPhase section */ 356 | 357 | /* Begin PBXSourcesBuildPhase section */ 358 | 7160C74E1F8FA4F300506FE1 /* Sources */ = { 359 | isa = PBXSourcesBuildPhase; 360 | buildActionMask = 2147483647; 361 | files = ( 362 | 71C590591F9B08A50098542B /* Gist.swift in Sources */, 363 | 7160C7931F8FA62800506FE1 /* ListGistsWorker.swift in Sources */, 364 | 7160C78E1F8FA62800506FE1 /* ListGistsInteractor.swift in Sources */, 365 | 7160C7A01F8FA73600506FE1 /* GistAPI.swift in Sources */, 366 | 7160C7901F8FA62800506FE1 /* ListGistsPresenter.swift in Sources */, 367 | 7160C7561F8FA4F300506FE1 /* AppDelegate.swift in Sources */, 368 | 7160C78F1F8FA62800506FE1 /* ListGistsModels.swift in Sources */, 369 | 7160C75E1F8FA4F300506FE1 /* Gister.xcdatamodeld in Sources */, 370 | 7160C7921F8FA62800506FE1 /* ListGistsViewController.swift in Sources */, 371 | 7160C7911F8FA62800506FE1 /* ListGistsRouter.swift in Sources */, 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | 7160C7651F8FA4F300506FE1 /* Sources */ = { 376 | isa = PBXSourcesBuildPhase; 377 | buildActionMask = 2147483647; 378 | files = ( 379 | 7160C76E1F8FA4F300506FE1 /* GisterTests.swift in Sources */, 380 | 7160C7A51F90017D00506FE1 /* GistAPITest.swift in Sources */, 381 | 71D5A6C01F9AC1F500BFA7A7 /* Seeds.swift in Sources */, 382 | 7160C79D1F8FA68800506FE1 /* ListGistsWorkerTests.swift in Sources */, 383 | 7160C79C1F8FA68800506FE1 /* ListGistsViewControllerTests.swift in Sources */, 384 | 7160C79A1F8FA68800506FE1 /* ListGistsInteractorTests.swift in Sources */, 385 | 7160C79B1F8FA68800506FE1 /* ListGistsPresenterTests.swift in Sources */, 386 | ); 387 | runOnlyForDeploymentPostprocessing = 0; 388 | }; 389 | 7160C7701F8FA4F300506FE1 /* Sources */ = { 390 | isa = PBXSourcesBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | 7160C7791F8FA4F300506FE1 /* GisterUITests.swift in Sources */, 394 | ); 395 | runOnlyForDeploymentPostprocessing = 0; 396 | }; 397 | /* End PBXSourcesBuildPhase section */ 398 | 399 | /* Begin PBXTargetDependency section */ 400 | 7160C76B1F8FA4F300506FE1 /* PBXTargetDependency */ = { 401 | isa = PBXTargetDependency; 402 | target = 7160C7511F8FA4F300506FE1 /* Gister */; 403 | targetProxy = 7160C76A1F8FA4F300506FE1 /* PBXContainerItemProxy */; 404 | }; 405 | 7160C7761F8FA4F300506FE1 /* PBXTargetDependency */ = { 406 | isa = PBXTargetDependency; 407 | target = 7160C7511F8FA4F300506FE1 /* Gister */; 408 | targetProxy = 7160C7751F8FA4F300506FE1 /* PBXContainerItemProxy */; 409 | }; 410 | /* End PBXTargetDependency section */ 411 | 412 | /* Begin PBXVariantGroup section */ 413 | 7160C7591F8FA4F300506FE1 /* Main.storyboard */ = { 414 | isa = PBXVariantGroup; 415 | children = ( 416 | 7160C75A1F8FA4F300506FE1 /* Base */, 417 | ); 418 | name = Main.storyboard; 419 | sourceTree = ""; 420 | }; 421 | 7160C7611F8FA4F300506FE1 /* LaunchScreen.storyboard */ = { 422 | isa = PBXVariantGroup; 423 | children = ( 424 | 7160C7621F8FA4F300506FE1 /* Base */, 425 | ); 426 | name = LaunchScreen.storyboard; 427 | sourceTree = ""; 428 | }; 429 | /* End PBXVariantGroup section */ 430 | 431 | /* Begin XCBuildConfiguration section */ 432 | 7160C77B1F8FA4F300506FE1 /* Debug */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | ALWAYS_SEARCH_USER_PATHS = NO; 436 | CLANG_ANALYZER_NONNULL = YES; 437 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 438 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 439 | CLANG_CXX_LIBRARY = "libc++"; 440 | CLANG_ENABLE_MODULES = YES; 441 | CLANG_ENABLE_OBJC_ARC = YES; 442 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 443 | CLANG_WARN_BOOL_CONVERSION = YES; 444 | CLANG_WARN_COMMA = YES; 445 | CLANG_WARN_CONSTANT_CONVERSION = YES; 446 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 447 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 448 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 449 | CLANG_WARN_EMPTY_BODY = YES; 450 | CLANG_WARN_ENUM_CONVERSION = YES; 451 | CLANG_WARN_INFINITE_RECURSION = YES; 452 | CLANG_WARN_INT_CONVERSION = YES; 453 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 454 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 455 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 456 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 457 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 458 | CLANG_WARN_STRICT_PROTOTYPES = YES; 459 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 460 | CLANG_WARN_UNREACHABLE_CODE = YES; 461 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 462 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 463 | COPY_PHASE_STRIP = NO; 464 | DEBUG_INFORMATION_FORMAT = dwarf; 465 | ENABLE_STRICT_OBJC_MSGSEND = YES; 466 | ENABLE_TESTABILITY = YES; 467 | GCC_C_LANGUAGE_STANDARD = gnu99; 468 | GCC_DYNAMIC_NO_PIC = NO; 469 | GCC_NO_COMMON_BLOCKS = YES; 470 | GCC_OPTIMIZATION_LEVEL = 0; 471 | GCC_PREPROCESSOR_DEFINITIONS = ( 472 | "DEBUG=1", 473 | "$(inherited)", 474 | ); 475 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 476 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 477 | GCC_WARN_UNDECLARED_SELECTOR = YES; 478 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 479 | GCC_WARN_UNUSED_FUNCTION = YES; 480 | GCC_WARN_UNUSED_VARIABLE = YES; 481 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 482 | MTL_ENABLE_DEBUG_INFO = YES; 483 | ONLY_ACTIVE_ARCH = YES; 484 | SDKROOT = iphoneos; 485 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 486 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 487 | TARGETED_DEVICE_FAMILY = "1,2"; 488 | }; 489 | name = Debug; 490 | }; 491 | 7160C77C1F8FA4F300506FE1 /* Release */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | ALWAYS_SEARCH_USER_PATHS = NO; 495 | CLANG_ANALYZER_NONNULL = YES; 496 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 497 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 498 | CLANG_CXX_LIBRARY = "libc++"; 499 | CLANG_ENABLE_MODULES = YES; 500 | CLANG_ENABLE_OBJC_ARC = YES; 501 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 502 | CLANG_WARN_BOOL_CONVERSION = YES; 503 | CLANG_WARN_COMMA = YES; 504 | CLANG_WARN_CONSTANT_CONVERSION = YES; 505 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 506 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 507 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 508 | CLANG_WARN_EMPTY_BODY = YES; 509 | CLANG_WARN_ENUM_CONVERSION = YES; 510 | CLANG_WARN_INFINITE_RECURSION = YES; 511 | CLANG_WARN_INT_CONVERSION = YES; 512 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 513 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 514 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 515 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 517 | CLANG_WARN_STRICT_PROTOTYPES = YES; 518 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 519 | CLANG_WARN_UNREACHABLE_CODE = YES; 520 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 521 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 522 | COPY_PHASE_STRIP = NO; 523 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 524 | ENABLE_NS_ASSERTIONS = NO; 525 | ENABLE_STRICT_OBJC_MSGSEND = YES; 526 | GCC_C_LANGUAGE_STANDARD = gnu99; 527 | GCC_NO_COMMON_BLOCKS = YES; 528 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 529 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 530 | GCC_WARN_UNDECLARED_SELECTOR = YES; 531 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 532 | GCC_WARN_UNUSED_FUNCTION = YES; 533 | GCC_WARN_UNUSED_VARIABLE = YES; 534 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 535 | MTL_ENABLE_DEBUG_INFO = NO; 536 | SDKROOT = iphoneos; 537 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 538 | TARGETED_DEVICE_FAMILY = "1,2"; 539 | VALIDATE_PRODUCT = YES; 540 | }; 541 | name = Release; 542 | }; 543 | 7160C77E1F8FA4F300506FE1 /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 547 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 548 | INFOPLIST_FILE = Gister/Info.plist; 549 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 550 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.Gister"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 553 | SWIFT_VERSION = 4.0; 554 | }; 555 | name = Debug; 556 | }; 557 | 7160C77F1F8FA4F300506FE1 /* Release */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 561 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 562 | INFOPLIST_FILE = Gister/Info.plist; 563 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 564 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.Gister"; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 567 | SWIFT_VERSION = 4.0; 568 | }; 569 | name = Release; 570 | }; 571 | 7160C7811F8FA4F300506FE1 /* Debug */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 575 | BUNDLE_LOADER = "$(TEST_HOST)"; 576 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 577 | INFOPLIST_FILE = GisterTests/Info.plist; 578 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 579 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.GisterTests"; 580 | PRODUCT_NAME = "$(TARGET_NAME)"; 581 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 582 | SWIFT_VERSION = 4.0; 583 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gister.app/Gister"; 584 | }; 585 | name = Debug; 586 | }; 587 | 7160C7821F8FA4F300506FE1 /* Release */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 591 | BUNDLE_LOADER = "$(TEST_HOST)"; 592 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 593 | INFOPLIST_FILE = GisterTests/Info.plist; 594 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 595 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.GisterTests"; 596 | PRODUCT_NAME = "$(TARGET_NAME)"; 597 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 598 | SWIFT_VERSION = 4.0; 599 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gister.app/Gister"; 600 | }; 601 | name = Release; 602 | }; 603 | 7160C7841F8FA4F300506FE1 /* Debug */ = { 604 | isa = XCBuildConfiguration; 605 | buildSettings = { 606 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 607 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 608 | INFOPLIST_FILE = GisterUITests/Info.plist; 609 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 610 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.GisterUITests"; 611 | PRODUCT_NAME = "$(TARGET_NAME)"; 612 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 613 | SWIFT_VERSION = 4.0; 614 | TEST_TARGET_NAME = Gister; 615 | }; 616 | name = Debug; 617 | }; 618 | 7160C7851F8FA4F300506FE1 /* Release */ = { 619 | isa = XCBuildConfiguration; 620 | buildSettings = { 621 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 622 | DEVELOPMENT_TEAM = 5LBZDN8XB8; 623 | INFOPLIST_FILE = GisterUITests/Info.plist; 624 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 625 | PRODUCT_BUNDLE_IDENTIFIER = "com.clean-swift.GisterUITests"; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 628 | SWIFT_VERSION = 4.0; 629 | TEST_TARGET_NAME = Gister; 630 | }; 631 | name = Release; 632 | }; 633 | /* End XCBuildConfiguration section */ 634 | 635 | /* Begin XCConfigurationList section */ 636 | 7160C74D1F8FA4F300506FE1 /* Build configuration list for PBXProject "Gister" */ = { 637 | isa = XCConfigurationList; 638 | buildConfigurations = ( 639 | 7160C77B1F8FA4F300506FE1 /* Debug */, 640 | 7160C77C1F8FA4F300506FE1 /* Release */, 641 | ); 642 | defaultConfigurationIsVisible = 0; 643 | defaultConfigurationName = Release; 644 | }; 645 | 7160C77D1F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "Gister" */ = { 646 | isa = XCConfigurationList; 647 | buildConfigurations = ( 648 | 7160C77E1F8FA4F300506FE1 /* Debug */, 649 | 7160C77F1F8FA4F300506FE1 /* Release */, 650 | ); 651 | defaultConfigurationIsVisible = 0; 652 | defaultConfigurationName = Release; 653 | }; 654 | 7160C7801F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "GisterTests" */ = { 655 | isa = XCConfigurationList; 656 | buildConfigurations = ( 657 | 7160C7811F8FA4F300506FE1 /* Debug */, 658 | 7160C7821F8FA4F300506FE1 /* Release */, 659 | ); 660 | defaultConfigurationIsVisible = 0; 661 | defaultConfigurationName = Release; 662 | }; 663 | 7160C7831F8FA4F300506FE1 /* Build configuration list for PBXNativeTarget "GisterUITests" */ = { 664 | isa = XCConfigurationList; 665 | buildConfigurations = ( 666 | 7160C7841F8FA4F300506FE1 /* Debug */, 667 | 7160C7851F8FA4F300506FE1 /* Release */, 668 | ); 669 | defaultConfigurationIsVisible = 0; 670 | defaultConfigurationName = Release; 671 | }; 672 | /* End XCConfigurationList section */ 673 | 674 | /* Begin XCVersionGroup section */ 675 | 7160C75C1F8FA4F300506FE1 /* Gister.xcdatamodeld */ = { 676 | isa = XCVersionGroup; 677 | children = ( 678 | 7160C75D1F8FA4F300506FE1 /* Gister.xcdatamodel */, 679 | ); 680 | currentVersion = 7160C75D1F8FA4F300506FE1 /* Gister.xcdatamodel */; 681 | path = Gister.xcdatamodeld; 682 | sourceTree = ""; 683 | versionGroupType = wrapper.xcdatamodel; 684 | }; 685 | /* End XCVersionGroup section */ 686 | }; 687 | rootObject = 7160C74A1F8FA4F300506FE1 /* Project object */; 688 | } 689 | --------------------------------------------------------------------------------