├── Clean Swift ├── Scene.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── UIViewController │ │ ├── ___FILEBASENAME___Worker.swift │ │ ├── ___FILEBASENAME___Router.swift │ │ ├── ___FILEBASENAME___Models.swift │ │ ├── ___FILEBASENAME___Presenter.swift │ │ ├── ___FILEBASENAME___Interactor.swift │ │ └── ___FILEBASENAME___ViewController.swift │ └── TemplateInfo.plist ├── Models.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ └── ___FILEBASENAME___Models.swift ├── Router.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── ___FILEBASENAME___Router.swift │ └── TemplateInfo.plist ├── Worker.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── ___FILEBASENAME___Worker.swift │ └── TemplateInfo.plist ├── Interactor.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ └── ___FILEBASENAME___Interactor.swift ├── Presenter.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ └── ___FILEBASENAME___Presenter.swift ├── View Controller.xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ └── UIViewController │ │ └── ___FILEBASENAME___ViewController.swift ├── Unit Tests (XCTest).xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── ___FILEBASENAME___WorkerTests.swift │ ├── TemplateInfo.plist │ ├── ___FILEBASENAME___InteractorTests.swift │ ├── ___FILEBASENAME___PresenterTests.swift │ └── ___FILEBASENAME___ViewControllerTests.swift └── Unit Tests (Quick-Nimble).xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── ___FILEBASENAME___WorkerSpec.swift │ ├── TemplateInfo.plist │ ├── ___FILEBASENAME___PresenterSpec.swift │ ├── ___FILEBASENAME___InteractorSpec.swift │ └── ___FILEBASENAME___ViewControllerSpec.swift ├── Makefile ├── LICENSE ├── README.md └── CHANGELOG.md /Clean Swift/Scene.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Scene.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Models.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Models.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Router.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Router.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Scene.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Worker.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Worker.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Interactor.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Interactor.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Models.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Models.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Presenter.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Presenter.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Router.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Router.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Worker.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Worker.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Interactor.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Interactor.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Presenter.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Presenter.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/View Controller.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/View Controller.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/View Controller.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/View Controller.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Unit Tests (XCTest).xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Unit Tests (XCTest).xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Unit Tests (Quick-Nimble).xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakej/clean-swift-template/HEAD/Clean Swift/Unit Tests (Quick-Nimble).xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | XCODE_USER_TEMPLATES_DIR=~/Library/Developer/Xcode/Templates/File\ Templates 2 | XCODE_USER_SNIPPETS_DIR=~/Library/Developer/Xcode/UserData/CodeSnippets 3 | 4 | TEMPLATES_DIR=Clean\ Swift 5 | 6 | install_templates: 7 | mkdir -p $(XCODE_USER_TEMPLATES_DIR) 8 | rm -fR $(XCODE_USER_TEMPLATES_DIR)/$(TEMPLATES_DIR) 9 | cp -R $(TEMPLATES_DIR) $(XCODE_USER_TEMPLATES_DIR) 10 | 11 | uninstall_templates: 12 | rm -fR $(XCODE_USER_TEMPLATES_DIR)/$(TEMPLATES_DIR) 13 | -------------------------------------------------------------------------------- /Clean Swift/Worker.xctemplate/___FILEBASENAME___Worker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ___VARIABLE_sceneName___Worker { 12 | 13 | // MARK: - Properties 14 | 15 | typealias Models = ___VARIABLE_sceneName___Models 16 | 17 | // MARK: - Methods 18 | 19 | // MARK: Screen Specific Validation 20 | 21 | func validate(exampleVariable: String?) -> Models.___VARIABLE_sceneName___Error? { 22 | var error: Models.___VARIABLE_sceneName___Error? 23 | 24 | if exampleVariable?.isEmpty == false { 25 | error = nil 26 | } else { 27 | error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 28 | } 29 | 30 | return error 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___Worker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ___VARIABLE_sceneName___Worker { 12 | 13 | // MARK: - Properties 14 | 15 | typealias Models = ___VARIABLE_sceneName___Models 16 | 17 | // MARK: - Methods 18 | 19 | // MARK: Screen Specific Validation 20 | 21 | func validate(exampleVariable: String?) -> Models.___VARIABLE_sceneName___Error? { 22 | var error: Models.___VARIABLE_sceneName___Error? 23 | 24 | if exampleVariable?.isEmpty == false { 25 | error = nil 26 | } else { 27 | error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 28 | } 29 | 30 | return error 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2020 Zaim Ramlan 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. -------------------------------------------------------------------------------- /Clean Swift/Router.xctemplate/___FILEBASENAME___Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___RoutingLogic { 12 | func routeToNext() 13 | } 14 | 15 | protocol ___VARIABLE_sceneName___DataPassing { 16 | var dataStore: ___VARIABLE_sceneName___DataStore? { get } 17 | } 18 | 19 | class ___VARIABLE_sceneName___Router: NSObject, ___VARIABLE_sceneName___RoutingLogic, ___VARIABLE_sceneName___DataPassing { 20 | 21 | // MARK: - Properties 22 | 23 | weak var viewController: ___VARIABLE_sceneName___ViewController? 24 | var dataStore: ___VARIABLE_sceneName___DataStore? 25 | 26 | // MARK: - Routing 27 | 28 | func routeToNext() { 29 | // let destinationVC = UIStoryboard(name: "", bundle: nil).instantiateViewController(withIdentifier: "") as! NextViewController 30 | // var destinationDS = destinationVC.router!.dataStore! 31 | // passDataTo(destinationDS, from: dataStore!) 32 | // viewController?.navigationController?.pushViewController(destinationVC, animated: true) 33 | } 34 | 35 | // MARK: - Data Passing 36 | 37 | // func passDataTo(_ destinationDS: inout NextDataStore, from sourceDS: ___VARIABLE_sceneName___DataStore) { 38 | // destinationDS.attribute = sourceDS.attribute 39 | // } 40 | } 41 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___RoutingLogic { 12 | func routeToNext() 13 | } 14 | 15 | protocol ___VARIABLE_sceneName___DataPassing { 16 | var dataStore: ___VARIABLE_sceneName___DataStore? { get } 17 | } 18 | 19 | class ___VARIABLE_sceneName___Router: NSObject, ___VARIABLE_sceneName___RoutingLogic, ___VARIABLE_sceneName___DataPassing { 20 | 21 | // MARK: - Properties 22 | 23 | weak var viewController: ___VARIABLE_sceneName___ViewController? 24 | var dataStore: ___VARIABLE_sceneName___DataStore? 25 | 26 | // MARK: - Routing 27 | 28 | func routeToNext() { 29 | // let destinationVC = UIStoryboard(name: "", bundle: nil).instantiateViewController(withIdentifier: "") as! NextViewController 30 | // var destinationDS = destinationVC.router!.dataStore! 31 | // passDataTo(destinationDS, from: dataStore!) 32 | // viewController?.navigationController?.pushViewController(destinationVC, animated: true) 33 | } 34 | 35 | // MARK: - Data Passing 36 | 37 | // func passDataTo(_ destinationDS: inout NextDataStore, from sourceDS: ___VARIABLE_sceneName___DataStore) { 38 | // destinationDS.attribute = sourceDS.attribute 39 | // } 40 | } 41 | -------------------------------------------------------------------------------- /Clean Swift/Models.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new set of boundary models between the view controller, interactor, presenter, and router using Uncle Bob's clean architecture. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Platforms 37 | 38 | com.apple.platform.iphoneos 39 | 40 | SortOrder 41 | 4 42 | Summary 43 | This generates a new set of boundary models between the view controller, interactor, presenter, and router using Uncle Bob's clean architecture. 44 | 45 | -------------------------------------------------------------------------------- /Clean Swift/Worker.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new worker to perform your specific business logic. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___Worker 38 | Description 39 | The worker name 40 | Identifier 41 | workerName 42 | Name 43 | Worker Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Platforms 51 | 52 | com.apple.platform.iphoneos 53 | 54 | SortOrder 55 | 5 56 | Summary 57 | This generates a new worker to perform your specific business logic. 58 | 59 | -------------------------------------------------------------------------------- /Clean Swift/Interactor.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new interactor using Uncle Bob's clean architecture. Your business logic goes here. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___Interactor 38 | Description 39 | The interactor name 40 | Identifier 41 | interactorName 42 | Name 43 | Interactor Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Platforms 51 | 52 | com.apple.platform.iphoneos 53 | 54 | SortOrder 55 | 2 56 | Summary 57 | This generates a new interactor using Uncle Bob's clean architecture. Your business logic goes here. 58 | 59 | -------------------------------------------------------------------------------- /Clean Swift/Presenter.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new presenter using Uncle Bob's clean architecture. Your presentation logic goes here. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___Presenter 38 | Description 39 | The presenter name 40 | Identifier 41 | presenterName 42 | Name 43 | Presenter Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Platforms 51 | 52 | com.apple.platform.iphoneos 53 | 54 | SortOrder 55 | 3 56 | Summary 57 | This generates a new presenter using Uncle Bob's clean architecture. Your presentation logic goes here. 58 | 59 | -------------------------------------------------------------------------------- /Clean Swift/Router.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new router using Uncle Bob's clean architecture. You navigate to other scenes by presenting and dismissing other view controllers here. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___Router 38 | Description 39 | The router name 40 | Identifier 41 | routerName 42 | Name 43 | Router Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Platforms 51 | 52 | com.apple.platform.iphoneos 53 | 54 | SortOrder 55 | 6 56 | Summary 57 | This generates a new router using Uncle Bob's clean architecture. You navigate to other scenes by presenting and dismissing other view controllers here. 58 | 59 | -------------------------------------------------------------------------------- /Clean Swift/Models.xctemplate/___FILEBASENAME___Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum ___VARIABLE_sceneName___Models { 12 | 13 | // MARK: - Use Cases 14 | 15 | enum FetchFromLocalDataStore { 16 | struct Request { 17 | } 18 | 19 | struct Response { 20 | } 21 | 22 | struct ViewModel { 23 | var exampleTranslation: String? 24 | } 25 | } 26 | 27 | enum FetchFromRemoteDataStore { 28 | struct Request { 29 | } 30 | 31 | struct Response { 32 | var exampleVariable: String? 33 | } 34 | 35 | struct ViewModel { 36 | var exampleVariable: String? 37 | } 38 | } 39 | 40 | enum TrackAnalytics { 41 | struct Request { 42 | var event: AnalyticsEvents 43 | } 44 | 45 | struct Response { 46 | } 47 | 48 | struct ViewModel { 49 | } 50 | } 51 | 52 | enum Perform___VARIABLE_sceneName___ { 53 | struct Request { 54 | var exampleVariable: String? 55 | } 56 | 57 | struct Response { 58 | var error: ___VARIABLE_sceneName___Error? 59 | } 60 | 61 | struct ViewModel { 62 | var error: ___VARIABLE_sceneName___Error? 63 | } 64 | } 65 | 66 | // MARK: - Types 67 | 68 | // replace with `AnalyticsEvents` with `AnalyticsConstants` if needed 69 | typealias AnalyticsEvents = ExampleAnalyticsEvents 70 | typealias ___VARIABLE_sceneName___Error = Error<___VARIABLE_sceneName___ErrorType> 71 | 72 | enum ExampleAnalyticsEvents { 73 | case screenView 74 | } 75 | 76 | enum ___VARIABLE_sceneName___ErrorType { 77 | case emptyExampleVariable, networkError 78 | } 79 | 80 | struct Error { 81 | var type: T 82 | var message: String? 83 | 84 | init(type: T) { 85 | self.type = type 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum ___VARIABLE_sceneName___Models { 12 | 13 | // MARK: - Use Cases 14 | 15 | enum FetchFromLocalDataStore { 16 | struct Request { 17 | } 18 | 19 | struct Response { 20 | } 21 | 22 | struct ViewModel { 23 | var exampleTranslation: String? 24 | } 25 | } 26 | 27 | enum FetchFromRemoteDataStore { 28 | struct Request { 29 | } 30 | 31 | struct Response { 32 | var exampleVariable: String? 33 | } 34 | 35 | struct ViewModel { 36 | var exampleVariable: String? 37 | } 38 | } 39 | 40 | enum TrackAnalytics { 41 | struct Request { 42 | var event: AnalyticsEvents 43 | } 44 | 45 | struct Response { 46 | } 47 | 48 | struct ViewModel { 49 | } 50 | } 51 | 52 | enum Perform___VARIABLE_sceneName___ { 53 | struct Request { 54 | var exampleVariable: String? 55 | } 56 | 57 | struct Response { 58 | var error: ___VARIABLE_sceneName___Error? 59 | } 60 | 61 | struct ViewModel { 62 | var error: ___VARIABLE_sceneName___Error? 63 | } 64 | } 65 | 66 | // MARK: - Types 67 | 68 | // replace with `AnalyticsEvents` with `AnalyticsConstants` if needed 69 | typealias AnalyticsEvents = ExampleAnalyticsEvents 70 | typealias ___VARIABLE_sceneName___Error = Error<___VARIABLE_sceneName___ErrorType> 71 | 72 | enum ExampleAnalyticsEvents { 73 | case screenView 74 | } 75 | 76 | enum ___VARIABLE_sceneName___ErrorType { 77 | case emptyExampleVariable, networkError 78 | } 79 | 80 | struct Error { 81 | var type: T 82 | var message: String? 83 | 84 | init(type: T) { 85 | self.type = type 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanSwift Template 2 | 3 | This is a CleanSwift template modified from the original, to accomodate common project usage. 4 | 5 | ## Modifications 6 | 7 | 1. All Files 8 | - All file templates are updated so that upon scene generation, the app still works without throwing errors. 9 | - Pre-built with 4 common use cases: 10 | 1. Upon entering a screen - `fetchFromLocalDataStore(with:)` 11 | - There's almost always local data to be displayed on the screen. 12 | 1. Upon entering a screen - `fetchFromRemoteDataStore(with:)` 13 | - Sometimes a data need to be fetched from an external source. 14 | - This use case decouples from local data fetching use case and allows it to be reused if needed (ie. Pull-To-Refresh). 15 | 1. Tracking analytics - `trackAnalytics(with:)` 16 | - Sometimes a triggered UI element needs to be tracked for app improvements. 17 | - This use case made it easier. 18 | 1. Upon leaving a screen - `perform(with:)` 19 | - In order to leave a screen, a use case is almost always needed. 20 | 21 | 1. Models 22 | - The template generates a file appended with the word `models` to make it more obvious. 23 | 24 | 1. Interactor, Presenter & ViewController 25 | - Each Use Case logic would have the comment header `// MARK: - Use Case -` for better readability between the Use Case & other functions. 26 | 27 | 1. Unit Test 28 | - The templates are also updated to test the pre-built use cases. 29 | - A template that is written using the Quick and Nimble libraries are also added if it's preferred over XCTest. 30 | 31 | 1. Removed 32 | - `UICollectionViewController` and `UITableViewController` templates are removed since the templates use the `UIViewController` templates anyway. 33 | 34 | ## Installation 35 | 36 | 1. Clone this repository 37 | 1. From this repository: 38 | 39 | To install the Clean Swift Xcode templates,run: 40 | ```bash 41 | $ make install_templates 42 | ``` 43 | 44 | To uninstall the Clean Swift Xcode templates, run: 45 | ```bash 46 | $ make uninstall_templates 47 | ``` 48 | 49 | ## References 50 | 51 | To learn more about Clean Swift and the VIP cycle, read: 52 | 53 | - http://clean-swift.com/clean-swift-ios-architecture 54 | 55 | There is a sample app available at: 56 | 57 | - https://github.com/zaimramlan/ios-clean-todo 58 | -------------------------------------------------------------------------------- /Clean Swift/View Controller.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new view controller using Uncle Bob's clean architecture. Your view/view controller logic goes here. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___ViewController 38 | Description 39 | The view controller name 40 | Identifier 41 | viewControllerName 42 | Name 43 | View Controller Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Default 51 | UIViewController 52 | Description 53 | What view controller class to subclass for the new scene 54 | FallbackHeader 55 | #import <UIKit/UIKit.h> 56 | Identifier 57 | viewControllerSubclass 58 | Name 59 | Subclass of: 60 | NotPersisted 61 | 62 | Required 63 | YES 64 | Type 65 | class 66 | Values 67 | 68 | UIViewController 69 | 70 | 71 | 72 | Platforms 73 | 74 | com.apple.platform.iphoneos 75 | 76 | SortOrder 77 | 1 78 | Summary 79 | This generates a new view controller using Uncle Bob's clean architecture. Your view/view controller logic goes here. 80 | 81 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/___FILEBASENAME___WorkerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | @testable import ___PROJECTNAMEASIDENTIFIER___ 10 | import XCTest 11 | 12 | class ___VARIABLE_sceneName___WorkerTests: XCTestCase { 13 | 14 | // MARK: - Subject Under Test (SUT) 15 | 16 | typealias Models = ___VARIABLE_sceneName___Models 17 | var sut: ___VARIABLE_sceneName___Worker! 18 | 19 | // MARK: - Test Lifecycle 20 | 21 | override func setUp() { 22 | super.setUp() 23 | setup___VARIABLE_sceneName___Worker() 24 | } 25 | 26 | override func tearDown() { 27 | sut = nil 28 | super.tearDown() 29 | } 30 | 31 | // MARK: - Test Setup 32 | 33 | func setup___VARIABLE_sceneName___Worker() { 34 | sut = ___VARIABLE_sceneName___Worker() 35 | } 36 | 37 | // MARK: - Test Doubles 38 | 39 | // MARK: - Tests 40 | 41 | func testValidateExampleVariableShouldCreateEmptyExampleVariableErrorIfExampleVariableIsNil() { 42 | // given 43 | let exampleVariable: String? = nil 44 | 45 | // when 46 | let error = sut.validate(exampleVariable: exampleVariable) 47 | 48 | // then 49 | XCTAssertNotNil(error, "validate(exampleVariable:) should create an error if example variable is nil") 50 | XCTAssertEqual(error?.type, Models.___VARIABLE_sceneName___ErrorType.emptyExampleVariable, "validate(exampleVariable:) should create an emptyExampleVariable error if example variable is nil") 51 | } 52 | 53 | func testValidateExampleVariableShouldCreateEmptyExampleVariableErrorIfExampleVariableIsEmpty() { 54 | // given 55 | let exampleVariable = "" 56 | 57 | // when 58 | let error = sut.validate(exampleVariable: exampleVariable) 59 | 60 | // then 61 | XCTAssertNotNil(error, "validate(exampleVariable:) should create an error if example variable is empty") 62 | XCTAssertEqual(error?.type, Models.___VARIABLE_sceneName___ErrorType.emptyExampleVariable, "validate(exampleVariable:) should create an emptyExampleVariable error if example variable is empty") 63 | } 64 | 65 | func testValidateExampleVariableShouldNotCreateErrorIfExampleVariableIsNotNilOrEmpty() { 66 | // given 67 | let exampleVariable = "Example string." 68 | 69 | // when 70 | let error = sut.validate(exampleVariable: exampleVariable) 71 | 72 | // then 73 | XCTAssertNil(error, "validate(exampleVariable:) should not create an error if example variable is not nil or empty") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/___FILEBASENAME___WorkerSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import ___PROJECTNAMEASIDENTIFIER___ 12 | 13 | class ___VARIABLE_sceneName___WorkerSpec: QuickSpec { 14 | override func spec() { 15 | 16 | // MARK: - Subject Under Test (SUT) 17 | 18 | typealias Models = ___VARIABLE_sceneName___Models 19 | var sut: ___VARIABLE_sceneName___Worker! 20 | 21 | // MARK: - Test Doubles 22 | 23 | // MARK: - Tests 24 | 25 | beforeEach { 26 | setupWorker() 27 | } 28 | 29 | afterEach { 30 | sut = nil 31 | } 32 | 33 | // MARK: - Methods 34 | 35 | describe("validate example variable") { 36 | context("example variable is nil", closure: { 37 | it("should create empty example variable error", closure: { 38 | // given 39 | let exampleVariable: String? = nil 40 | 41 | // when 42 | let error = sut.validate(exampleVariable: exampleVariable) 43 | 44 | // then 45 | expect(error).notTo(beNil()) 46 | expect(error?.type).to(equal(Models.___VARIABLE_sceneName___ErrorType.emptyExampleVariable)) 47 | }) 48 | }) 49 | 50 | context("example variable is empty", closure: { 51 | it("should create empty example variable error", closure: { 52 | // given 53 | let exampleVariable = "" 54 | 55 | // when 56 | let error = sut.validate(exampleVariable: exampleVariable) 57 | 58 | // then 59 | expect(error).notTo(beNil()) 60 | expect(error?.type).to(equal(Models.___VARIABLE_sceneName___ErrorType.emptyExampleVariable)) 61 | }) 62 | }) 63 | 64 | context("example variable is not nil and not empty", closure: { 65 | it("should not create error", closure: { 66 | // given 67 | let exampleVariable = "Example string." 68 | 69 | // when 70 | let error = sut.validate(exampleVariable: exampleVariable) 71 | 72 | // then 73 | expect(error).to(beNil()) 74 | }) 75 | }) 76 | } 77 | 78 | // MARK: - Test Helpers 79 | 80 | func setupWorker() { 81 | sut = ___VARIABLE_sceneName___Worker() 82 | } 83 | } 84 | } 85 | 86 | // MARK: - Test Doubles 87 | 88 | extension ___VARIABLE_sceneName___WorkerSpec { 89 | } 90 | -------------------------------------------------------------------------------- /Clean Swift/Presenter.xctemplate/___FILEBASENAME___Presenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___PresentationLogic { 12 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) 13 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) 14 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) 15 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) 16 | } 17 | 18 | class ___VARIABLE_sceneName___Presenter: ___VARIABLE_sceneName___PresentationLogic { 19 | 20 | // MARK: - Properties 21 | 22 | typealias Models = ___VARIABLE_sceneName___Models 23 | weak var viewController: ___VARIABLE_sceneName___DisplayLogic? 24 | 25 | // MARK: - Use Case - Fetch From Local DataStore 26 | 27 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) { 28 | let translation = "Some localized text." 29 | let viewModel = Models.FetchFromLocalDataStore.ViewModel(exampleTranslation: translation) 30 | viewController?.displayFetchFromLocalDataStore(with: viewModel) 31 | } 32 | 33 | // MARK: - Use Case - Fetch From Remote DataStore 34 | 35 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) { 36 | let formattedExampleVariable = response.exampleVariable ?? "" 37 | let viewModel = Models.FetchFromRemoteDataStore.ViewModel(exampleVariable: formattedExampleVariable) 38 | viewController?.displayFetchFromRemoteDataStore(with: viewModel) 39 | } 40 | 41 | // MARK: - Use Case - Track Analytics 42 | 43 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) { 44 | let viewModel = Models.TrackAnalytics.ViewModel() 45 | viewController?.displayTrackAnalytics(with: viewModel) 46 | } 47 | 48 | // MARK: - Use Case - ___VARIABLE_sceneName___ 49 | 50 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) { 51 | var responseError = response.error 52 | 53 | if let error = responseError { 54 | switch error.type { 55 | case .emptyExampleVariable: 56 | responseError?.message = "Localized empty/nil error message." 57 | 58 | case .networkError: 59 | responseError?.message = "Localized network error message." 60 | } 61 | } 62 | 63 | let viewModel = Models.Perform___VARIABLE_sceneName___.ViewModel(error: responseError) 64 | viewController?.displayPerform___VARIABLE_sceneName___(with: viewModel) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___Presenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___PresentationLogic { 12 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) 13 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) 14 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) 15 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) 16 | } 17 | 18 | class ___VARIABLE_sceneName___Presenter: ___VARIABLE_sceneName___PresentationLogic { 19 | 20 | // MARK: - Properties 21 | 22 | typealias Models = ___VARIABLE_sceneName___Models 23 | weak var viewController: ___VARIABLE_sceneName___DisplayLogic? 24 | 25 | // MARK: - Use Case - Fetch From Local DataStore 26 | 27 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) { 28 | let translation = "Some localized text." 29 | let viewModel = Models.FetchFromLocalDataStore.ViewModel(exampleTranslation: translation) 30 | viewController?.displayFetchFromLocalDataStore(with: viewModel) 31 | } 32 | 33 | // MARK: - Use Case - Fetch From Remote DataStore 34 | 35 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) { 36 | let formattedExampleVariable = response.exampleVariable ?? "" 37 | let viewModel = Models.FetchFromRemoteDataStore.ViewModel(exampleVariable: formattedExampleVariable) 38 | viewController?.displayFetchFromRemoteDataStore(with: viewModel) 39 | } 40 | 41 | // MARK: - Use Case - Track Analytics 42 | 43 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) { 44 | let viewModel = Models.TrackAnalytics.ViewModel() 45 | viewController?.displayTrackAnalytics(with: viewModel) 46 | } 47 | 48 | // MARK: - Use Case - ___VARIABLE_sceneName___ 49 | 50 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) { 51 | var responseError = response.error 52 | 53 | if let error = responseError { 54 | switch error.type { 55 | case .emptyExampleVariable: 56 | responseError?.message = "Localized empty/nil error message." 57 | 58 | case .networkError: 59 | responseError?.message = "Localized network error message." 60 | } 61 | } 62 | 63 | let viewModel = Models.Perform___VARIABLE_sceneName___.ViewModel(error: responseError) 64 | viewController?.displayPerform___VARIABLE_sceneName___(with: viewModel) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates unit tests for your scene using Uncle Bob's clean architecture. It consists of three test files for view controller, interactor, and presenter. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create tests for 16 | Identifier 17 | sceneName 18 | Name 19 | Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___ViewControllerSpec 38 | Description 39 | The view controller spec name 40 | Identifier 41 | viewControllerSpecName 42 | Name 43 | View Controller Spec Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Default 51 | ___VARIABLE_sceneName:identifier___InteractorSpec 52 | Description 53 | The interactor spec name 54 | Identifier 55 | interactorSpecName 56 | Name 57 | Interactor Spec Name: 58 | Required 59 | 60 | Type 61 | static 62 | 63 | 64 | Default 65 | ___VARIABLE_sceneName:identifier___PresenterSpec 66 | Description 67 | The presenter spec name 68 | Identifier 69 | presenterSpecName 70 | Name 71 | Presenter Spec Name: 72 | Required 73 | 74 | Type 75 | static 76 | 77 | 78 | Default 79 | ___VARIABLE_sceneName:identifier___WorkerSpec 80 | Description 81 | The worker spec name 82 | Identifier 83 | workerSpecName 84 | Name 85 | Worker Spec Name: 86 | Required 87 | 88 | Type 89 | static 90 | 91 | 92 | Platforms 93 | 94 | com.apple.platform.iphoneos 95 | 96 | SortOrder 97 | 7 98 | Summary 99 | This generates unit tests for your scene using Uncle Bob's clean architecture. 100 | 101 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates unit tests for your scene using Uncle Bob's clean architecture. It consists of three test files for view controller, interactor, and presenter. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create tests for 16 | Identifier 17 | sceneName 18 | Name 19 | Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___ViewControllerTests 38 | Description 39 | The view controller tests name 40 | Identifier 41 | viewControllerTestsName 42 | Name 43 | View Controller Tests Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Default 51 | ___VARIABLE_sceneName:identifier___InteractorTests 52 | Description 53 | The interactor tests name 54 | Identifier 55 | interactorTestsName 56 | Name 57 | Interactor Tests Name: 58 | Required 59 | 60 | Type 61 | static 62 | 63 | 64 | Default 65 | ___VARIABLE_sceneName:identifier___PresenterTests 66 | Description 67 | The presenter tests name 68 | Identifier 69 | presenterTestsName 70 | Name 71 | Presenter Tests Name: 72 | Required 73 | 74 | Type 75 | static 76 | 77 | 78 | Default 79 | ___VARIABLE_sceneName:identifier___WorkerTests 80 | Description 81 | The worker tests name 82 | Identifier 83 | workerTestsName 84 | Name 85 | Worker Tests Name: 86 | Required 87 | 88 | Type 89 | static 90 | 91 | 92 | Platforms 93 | 94 | com.apple.platform.iphoneos 95 | 96 | SortOrder 97 | 7 98 | Summary 99 | This generates unit tests for your scene using Uncle Bob's clean architecture. 100 | 101 | -------------------------------------------------------------------------------- /Clean Swift/Interactor.xctemplate/___FILEBASENAME___Interactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___BusinessLogic { 12 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) 13 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) 14 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) 15 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) 16 | } 17 | 18 | protocol ___VARIABLE_sceneName___DataStore { 19 | var exampleVariable: String? { get set } 20 | } 21 | 22 | class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___BusinessLogic, ___VARIABLE_sceneName___DataStore { 23 | 24 | // MARK: - Properties 25 | 26 | typealias Models = ___VARIABLE_sceneName___Models 27 | 28 | lazy var worker = ___VARIABLE_sceneName___Worker() 29 | var presenter: ___VARIABLE_sceneName___PresentationLogic? 30 | 31 | var exampleVariable: String? 32 | 33 | // MARK: - Use Case - Fetch From Local DataStore 34 | 35 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) { 36 | let response = Models.FetchFromLocalDataStore.Response() 37 | presenter?.presentFetchFromLocalDataStore(with: response) 38 | } 39 | 40 | // MARK: - Use Case - Fetch From Remote DataStore 41 | 42 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) { 43 | // fetch something from backend and return the values here 44 | // <#Network Worker Instance#>.fetchFromRemoteDataStore(completion: { [weak self] code in 45 | // let response = Models.FetchFromRemoteDataStore.Response(exampleVariable: code) 46 | // self?.presenter?.presentFetchFromRemoteDataStore(with: response) 47 | // }) 48 | } 49 | 50 | // MARK: - Use Case - Track Analytics 51 | 52 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) { 53 | // call analytics library/wrapper here to track analytics 54 | // <#Analytics Worker Instance#>.trackAnalytics(event: request.event) 55 | 56 | let response = Models.TrackAnalytics.Response() 57 | presenter?.presentTrackAnalytics(with: response) 58 | } 59 | 60 | // MARK: - Use Case - ___VARIABLE_sceneName___ 61 | 62 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) { 63 | let error = worker.validate(exampleVariable: request.exampleVariable) 64 | 65 | if let error = error { 66 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 67 | presenter?.presentPerform___VARIABLE_sceneName___(with: response) 68 | return 69 | } 70 | 71 | // <#Network Worker Instance#>.perform___VARIABLE_sceneName___(completion: { [weak self, weak request] isSuccessful, error in 72 | // self?.completion(request?.exampleVariable, isSuccessful, error) 73 | // }) 74 | } 75 | 76 | private func completion(_ exampleVariable: String?, _ isSuccessful: Bool, _ error: Models.___VARIABLE_sceneName___Error?) { 77 | if isSuccessful { 78 | // do something on success 79 | let goodExample = exampleVariable ?? "" 80 | self.exampleVariable = goodExample 81 | } 82 | 83 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 84 | presenter?.presentPerform___VARIABLE_sceneName___(with: response) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___Interactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___BusinessLogic { 12 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) 13 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) 14 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) 15 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) 16 | } 17 | 18 | protocol ___VARIABLE_sceneName___DataStore { 19 | var exampleVariable: String? { get set } 20 | } 21 | 22 | class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___BusinessLogic, ___VARIABLE_sceneName___DataStore { 23 | 24 | // MARK: - Properties 25 | 26 | typealias Models = ___VARIABLE_sceneName___Models 27 | 28 | lazy var worker = ___VARIABLE_sceneName___Worker() 29 | var presenter: ___VARIABLE_sceneName___PresentationLogic? 30 | 31 | var exampleVariable: String? 32 | 33 | // MARK: - Use Case - Fetch From Local DataStore 34 | 35 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) { 36 | let response = Models.FetchFromLocalDataStore.Response() 37 | presenter?.presentFetchFromLocalDataStore(with: response) 38 | } 39 | 40 | // MARK: - Use Case - Fetch From Remote DataStore 41 | 42 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) { 43 | // fetch something from backend and return the values here 44 | // <#Network Worker Instance#>.fetchFromRemoteDataStore(completion: { [weak self] code in 45 | // let response = Models.FetchFromRemoteDataStore.Response(exampleVariable: code) 46 | // self?.presenter?.presentFetchFromRemoteDataStore(with: response) 47 | // }) 48 | } 49 | 50 | // MARK: - Use Case - Track Analytics 51 | 52 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) { 53 | // call analytics library/wrapper here to track analytics 54 | // <#Analytics Worker Instance#>.trackAnalytics(event: request.event) 55 | 56 | let response = Models.TrackAnalytics.Response() 57 | presenter?.presentTrackAnalytics(with: response) 58 | } 59 | 60 | // MARK: - Use Case - ___VARIABLE_sceneName___ 61 | 62 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) { 63 | let error = worker.validate(exampleVariable: request.exampleVariable) 64 | 65 | if let error = error { 66 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 67 | presenter?.presentPerform___VARIABLE_sceneName___(with: response) 68 | return 69 | } 70 | 71 | // <#Network Worker Instance#>.perform___VARIABLE_sceneName___(completion: { [weak self, weak request] isSuccessful, error in 72 | // self?.completion(request?.exampleVariable, isSuccessful, error) 73 | // }) 74 | } 75 | 76 | private func completion(_ exampleVariable: String?, _ isSuccessful: Bool, _ error: Models.___VARIABLE_sceneName___Error?) { 77 | if isSuccessful { 78 | // do something on success 79 | let goodExample = exampleVariable ?? "" 80 | self.exampleVariable = goodExample 81 | } 82 | 83 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 84 | presenter?.presentPerform___VARIABLE_sceneName___(with: response) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file 4 | 5 | ## 5.4.0 6 | 7 | - All 8 | - Added `typealias Models` to shorten call to each screen's models 9 | - Business/Presentation/Display Protocols 10 | - Leave it to call the screen's model in full, to not unnecessary call a `typealias` from within Interactor, Presenter, ViewController respectively 11 | - Models 12 | - Modified `AnalyticsEvents` to `typealias` to hint to refer to an centralised analytics constants enum 13 | - Declared `typealias Error = Error` to simplify referencing to errors 14 | - Changed the comment header from `View Models` to `Types` to make more sense 15 | - Change `apiError` to `networkError` case to broaden the error scope 16 | - Interactor 17 | - Simplified `fetchFromRemoteDataStore`, `trackAnalytics` and `perform___VARIABLE_sceneName___` use cases to be able to call shared workers directly, instead of having to wrap it through the screen's worker 18 | - Changed referencing screen worker as `lazy` as there's no specific reason to make it optional 19 | - Router 20 | - Declared view controller as `weak` to prevent memory leaks 21 | - Unit Tests 22 | - Updated both XCTest and Quick-Nimble templates to reflect the above changes 23 | 24 | ## 5.3.0 25 | 26 | - Added screen view and primary button to analytics events 27 | - Moved track analytics to worker 28 | - Replace (c) with © to follow latest Apple templates 29 | - Added missing test cases in Interactor XCTests 30 | - Remove whitespaces 31 | - Split fetchFromDataStore use case into local and remote 32 | - Amend examples to make it apparent for local and remote data store fetching 33 | - Update unit tests to support local and remote data store fetching 34 | 35 | ## 5.2.1 36 | 37 | - Spied use case models for checking passed values 38 | - amend typos on some function signatures 39 | 40 | ## 5.2.0 41 | 42 | - Added TrackAnalytics use case 43 | - Update unit test templates to handle TrackAnalytics use case 44 | - Added unit test templates using quick and nimble 45 | 46 | ## 5.1.0 47 | 48 | - Updated templates with Perform use case 49 | - Updated unit test templates 50 | 51 | ## 5.0.0 52 | 53 | - Proper version release 54 | 55 | ## 4.0.3 56 | 57 | - Change indentations to 4 spaces 58 | 59 | ## 4.0.2 60 | 61 | - Treat Fetch from datastore as a Use Case for better clarity 62 | 63 | ## 4.0.1 64 | 65 | - Modified the Unit Test template to accommodate common project usage 66 | 67 | ## 4.0.0 68 | 69 | - Modified the original Clean Swift template to accommodate common project usage 70 | 71 | ## 3.0.2 72 | 73 | - Fixed @testable import for project names containing spaces 74 | 75 | ## 3.0.1 76 | 77 | - Added example unit tests for the sample use case in: 78 | - View controller 79 | - Interactor 80 | - Presenter 81 | 82 | ## 3.0.0 83 | 84 | - Updated for the latest Xcode 8.3.3 and Xcode 9.0 beta 85 | - Updated to work with Swift 3 and Swift 4 86 | - Improved routing whether you use segues or not 87 | - Improved data passing using the all new data store protocol 88 | - Separated the routing process into two phases: navigation and data passing, with a clean interface 89 | - Removed the need for configurator in favor of cleaner setup 90 | - Combined input and output protocols to remove duplication 91 | - Renamed protocols with better names 92 | - Swiftier models with nested enums and structs 93 | - Use optionals to prevent crash in the VIP cycle when the scene is no longer in memory 94 | - Works whether you use storyboards to build your UI or not 95 | - View controller class names are now recognized when specifying class names in storyboards 96 | 97 | ## 2.0.1 98 | 99 | - Updated unit tests for Swift 3 100 | 101 | ## 2.0.0 102 | 103 | - Nest model structs instead of using underscore 104 | - Swift 3 compatible 105 | 106 | ## 1.1.2 107 | 108 | - Added @testable import to unit test files 109 | - Fixed bug to get main bundle in view controller unit test setup 110 | 111 | ## 1.1.1 112 | 113 | - Added missing router input protocol conformance to router template 114 | 115 | ## 1.1.0 116 | 117 | - Added Unit Tests template to generate XCTest unit test files for: 118 | - View controller 119 | - Interactor 120 | - Presenter 121 | - Worker 122 | 123 | ## 1.0.0 124 | 125 | - Added the Scene template to generate the following Clean Swift components: 126 | - View Controller 127 | - Interactor 128 | - Presenter 129 | - Router 130 | - Worker 131 | - Models 132 | - Configurator 133 | - These components can also be generated individually 134 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DefaultCompletionName 6 | MyScene 7 | Description 8 | This generates a new scene using Uncle Bob's clean architecture. It consists of the view controller, interactor, presenter, and router. You can then create individual workers to supplement the interactor. 9 | Kind 10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 11 | Options 12 | 13 | 14 | Description 15 | The name of the scene to create 16 | Identifier 17 | sceneName 18 | Name 19 | New Scene Name: 20 | NotPersisted 21 | 22 | Required 23 | 24 | Type 25 | text 26 | 27 | 28 | Default 29 | ___VARIABLE_sceneName:identifier___ 30 | Identifier 31 | productName 32 | Type 33 | static 34 | 35 | 36 | Default 37 | ___VARIABLE_sceneName:identifier___ViewController 38 | Description 39 | The view controller name 40 | Identifier 41 | viewControllerName 42 | Name 43 | View Controller Name: 44 | Required 45 | 46 | Type 47 | static 48 | 49 | 50 | Default 51 | ___VARIABLE_sceneName:identifier___Interactor 52 | Description 53 | The interactor name 54 | Identifier 55 | interactorName 56 | Name 57 | Interactor Name: 58 | Required 59 | 60 | Type 61 | static 62 | 63 | 64 | Default 65 | ___VARIABLE_sceneName:identifier___Presenter 66 | Description 67 | The presenter name 68 | Identifier 69 | presenterName 70 | Name 71 | Presenter Name: 72 | Required 73 | 74 | Type 75 | static 76 | 77 | 78 | Default 79 | ___VARIABLE_sceneName:identifier___Router 80 | Description 81 | The router name 82 | Identifier 83 | routerName 84 | Name 85 | Router Name: 86 | Required 87 | 88 | Type 89 | static 90 | 91 | 92 | Default 93 | ___VARIABLE_sceneName:identifier___Configurator 94 | Description 95 | The configurator name 96 | Identifier 97 | configuratorName 98 | Name 99 | Configurator Name: 100 | Required 101 | 102 | Type 103 | static 104 | 105 | 106 | Default 107 | ___VARIABLE_sceneName:identifier___Worker 108 | Description 109 | The worker name 110 | Identifier 111 | workerName 112 | Name 113 | Worker Name: 114 | Required 115 | 116 | Type 117 | static 118 | 119 | 120 | Default 121 | ___VARIABLE_sceneName:identifier___Models 122 | Description 123 | The model file name 124 | Identifier 125 | modelFileName 126 | Name 127 | Model File Name: 128 | Required 129 | 130 | Type 131 | static 132 | 133 | 134 | Default 135 | UIViewController 136 | Description 137 | What view controller class to subclass for the new scene 138 | FallbackHeader 139 | #import <UIKit/UIKit.h> 140 | Identifier 141 | viewControllerSubclass 142 | Name 143 | Subclass of: 144 | NotPersisted 145 | 146 | Required 147 | YES 148 | Type 149 | class 150 | Values 151 | 152 | UIViewController 153 | 154 | 155 | 156 | Platforms 157 | 158 | com.apple.platform.iphoneos 159 | 160 | SortOrder 161 | 0 162 | Summary 163 | This generates a new scene using Uncle Bob's clean architecture. 164 | 165 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/___FILEBASENAME___InteractorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | @testable import ___PROJECTNAMEASIDENTIFIER___ 10 | import XCTest 11 | 12 | class ___VARIABLE_sceneName___InteractorTests: XCTestCase { 13 | 14 | // MARK: - Subject Under Test (SUT) 15 | 16 | typealias Models = ___VARIABLE_sceneName___Models 17 | var sut: ___VARIABLE_sceneName___Interactor! 18 | 19 | // MARK: - Test Lifecycle 20 | 21 | override func setUp() { 22 | super.setUp() 23 | setup___VARIABLE_sceneName___Interactor() 24 | } 25 | 26 | override func tearDown() { 27 | sut = nil 28 | super.tearDown() 29 | } 30 | 31 | // MARK: - Test Setup 32 | 33 | func setup___VARIABLE_sceneName___Interactor() { 34 | sut = ___VARIABLE_sceneName___Interactor() 35 | } 36 | 37 | // MARK: - Test Doubles 38 | 39 | class ___VARIABLE_sceneName___PresentationLogicSpy: ___VARIABLE_sceneName___PresentationLogic { 40 | 41 | // MARK: Spied Methods 42 | 43 | var presentFetchFromLocalDataStoreCalled = false 44 | var fetchFromLocalDataStoreResponse: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response! 45 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) { 46 | presentFetchFromLocalDataStoreCalled = true 47 | fetchFromLocalDataStoreResponse = response 48 | } 49 | 50 | var presentFetchFromRemoteDataStoreCalled = false 51 | var fetchFromRemoteDataStoreResponse: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response! 52 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) { 53 | presentFetchFromRemoteDataStoreCalled = true 54 | fetchFromRemoteDataStoreResponse = response 55 | } 56 | 57 | var presentTrackAnalyticsCalled = false 58 | var trackAnalyticsResponse: ___VARIABLE_sceneName___Models.TrackAnalytics.Response! 59 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) { 60 | presentTrackAnalyticsCalled = true 61 | trackAnalyticsResponse = response 62 | } 63 | 64 | var presentPerform___VARIABLE_sceneName___Called = false 65 | var perform___VARIABLE_sceneName___Response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response! 66 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) { 67 | presentPerform___VARIABLE_sceneName___Called = true 68 | perform___VARIABLE_sceneName___Response = response 69 | } 70 | } 71 | 72 | class ___VARIABLE_sceneName___WorkerSpy: ___VARIABLE_sceneName___Worker { 73 | 74 | // MARK: Spied Methods 75 | 76 | var validateExampleVariableCalled = false 77 | override func validate(exampleVariable: String?) -> Models.___VARIABLE_sceneName___Error? { 78 | validateExampleVariableCalled = true 79 | return super.validate(exampleVariable: exampleVariable) 80 | } 81 | } 82 | 83 | // MARK: - Tests 84 | 85 | func testFetchFromLocalDataStoreShouldAskPresenterToFormat() { 86 | // given 87 | let spy = ___VARIABLE_sceneName___PresentationLogicSpy() 88 | sut.presenter = spy 89 | let request = Models.FetchFromLocalDataStore.Request() 90 | 91 | // when 92 | sut.fetchFromLocalDataStore(with: request) 93 | 94 | // then 95 | XCTAssertTrue(spy.presentFetchFromLocalDataStoreCalled, "fetchFromLocalDataStore(with:) should ask the presenter to format the result") 96 | } 97 | 98 | func testTrackAnalyticsShouldAskPresenterToFormat() { 99 | // given 100 | let spy = ___VARIABLE_sceneName___PresentationLogicSpy() 101 | sut.presenter = spy 102 | let request = Models.TrackAnalytics.Request(event: .screenView) 103 | 104 | // when 105 | sut.trackAnalytics(with: request) 106 | 107 | // then 108 | XCTAssertTrue(spy.presentTrackAnalyticsCalled, "trackAnalytics(with:) should ask the presenter to format the result") 109 | } 110 | 111 | func testPerform___VARIABLE_sceneName___ShouldValidateExampleVariable() { 112 | // given 113 | let spy = ___VARIABLE_sceneName___WorkerSpy() 114 | sut.worker = spy 115 | let request = Models.Perform___VARIABLE_sceneName___.Request() 116 | 117 | // when 118 | sut.perform___VARIABLE_sceneName___(with: request) 119 | 120 | // then 121 | XCTAssertTrue(spy.validateExampleVariableCalled, "perform___VARIABLE_sceneName___(with:) should ask the worker to validate the example variable") 122 | } 123 | 124 | func testPerform___VARIABLE_sceneName___ShouldAskPresenterToFormat() { 125 | // given 126 | let spy = ___VARIABLE_sceneName___PresentationLogicSpy() 127 | sut.presenter = spy 128 | let request = Models.Perform___VARIABLE_sceneName___.Request() 129 | 130 | // when 131 | let expect = expectation(description: "Wait for perform___VARIABLE_sceneName___(with:) to return") 132 | sut.perform___VARIABLE_sceneName___(with: request) 133 | expect.fulfill() 134 | waitForExpectations(timeout: 1) 135 | 136 | // then 137 | XCTAssertTrue(spy.presentPerform___VARIABLE_sceneName___Called, "perform___VARIABLE_sceneName___(with:) should ask the presenter to format the result") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/___FILEBASENAME___PresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | @testable import ___PROJECTNAMEASIDENTIFIER___ 10 | import XCTest 11 | 12 | class ___VARIABLE_sceneName___PresenterTests: XCTestCase { 13 | 14 | // MARK: - Subject Under Test (SUT) 15 | 16 | typealias Models = ___VARIABLE_sceneName___Models 17 | var sut: ___VARIABLE_sceneName___Presenter! 18 | 19 | // MARK: - Test Lifecycle 20 | 21 | override func setUp() { 22 | super.setUp() 23 | setup___VARIABLE_sceneName___Presenter() 24 | } 25 | 26 | override func tearDown() { 27 | sut = nil 28 | super.tearDown() 29 | } 30 | 31 | // MARK: - Test Setup 32 | 33 | func setup___VARIABLE_sceneName___Presenter() { 34 | sut = ___VARIABLE_sceneName___Presenter() 35 | } 36 | 37 | // MARK: - Test Doubles 38 | 39 | class ___VARIABLE_sceneName___DisplayLogicSpy: ___VARIABLE_sceneName___DisplayLogic { 40 | 41 | // MARK: Spied Methods 42 | 43 | var displayFetchFromLocalDataStoreCalled = false 44 | var fetchFromLocalDataStoreViewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel! 45 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) { 46 | displayFetchFromLocalDataStoreCalled = true 47 | fetchFromLocalDataStoreViewModel = viewModel 48 | } 49 | 50 | var displayFetchFromRemoteDataStoreCalled = false 51 | var fetchFromRemoteDataStoreViewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel! 52 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) { 53 | displayFetchFromRemoteDataStoreCalled = true 54 | fetchFromRemoteDataStoreViewModel = viewModel 55 | } 56 | 57 | var displayTrackAnalyticsCalled = false 58 | var trackAnalyticsViewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel! 59 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) { 60 | displayTrackAnalyticsCalled = true 61 | trackAnalyticsViewModel = viewModel 62 | } 63 | 64 | var displayPerform___VARIABLE_sceneName___Called = false 65 | var perform___VARIABLE_sceneName___ViewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel! 66 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) { 67 | displayPerform___VARIABLE_sceneName___Called = true 68 | perform___VARIABLE_sceneName___ViewModel = viewModel 69 | } 70 | } 71 | 72 | // MARK: - Tests 73 | 74 | func testPresentFetchFromLocalDataStoreShouldAskTheViewControllerToDisplay() { 75 | // given 76 | let spy = ___VARIABLE_sceneName___DisplayLogicSpy() 77 | sut.viewController = spy 78 | let response = Models.FetchFromLocalDataStore.Response() 79 | 80 | // when 81 | sut.presentFetchFromLocalDataStore(with: response) 82 | 83 | // then 84 | XCTAssertTrue(spy.displayFetchFromLocalDataStoreCalled, "presentFetchFromLocalDataStore(with:) should ask the view controller to display the result") 85 | } 86 | 87 | func testPresentFetchFromRemoteDataStoreShouldAskTheViewControllerToDisplay() { 88 | // given 89 | let spy = ___VARIABLE_sceneName___DisplayLogicSpy() 90 | sut.viewController = spy 91 | let response = Models.FetchFromRemoteDataStore.Response() 92 | 93 | // when 94 | sut.presentFetchFromRemoteDataStore(with: response) 95 | 96 | // then 97 | XCTAssertTrue(spy.displayFetchFromRemoteDataStoreCalled, "presentFetchFromRemoteDataStore(with:) should ask the view controller to display the result") 98 | } 99 | 100 | func testPresentTrackAnalyticsShouldAskTheViewControllerToDisplay() { 101 | // given 102 | let spy = ___VARIABLE_sceneName___DisplayLogicSpy() 103 | sut.viewController = spy 104 | let response = Models.TrackAnalytics.Response() 105 | 106 | // when 107 | sut.presentTrackAnalytics(with: response) 108 | 109 | // then 110 | XCTAssertTrue(spy.displayTrackAnalyticsCalled, "presentTrackAnalytics(with:) should ask the view controller to display the result") 111 | } 112 | 113 | func testPresentPerform___VARIABLE_sceneName___ShouldAskTheViewControllerToDisplay() { 114 | // given 115 | let spy = ___VARIABLE_sceneName___DisplayLogicSpy() 116 | sut.viewController = spy 117 | let response = Models.Perform___VARIABLE_sceneName___.Response() 118 | 119 | // when 120 | sut.presentPerform___VARIABLE_sceneName___(with: response) 121 | 122 | // then 123 | XCTAssertTrue(spy.displayPerform___VARIABLE_sceneName___Called, "presentPerform___VARIABLE_sceneName___(with:) should ask the view controller to display the result") 124 | } 125 | 126 | func testPresentPerform___VARIABLE_sceneName___ShouldReturnErrorMessageIfThereIsAnError() { 127 | // given 128 | let error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 129 | let spy = ___VARIABLE_sceneName___DisplayLogicSpy() 130 | sut.viewController = spy 131 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 132 | 133 | // when 134 | sut.presentPerform___VARIABLE_sceneName___(with: response) 135 | 136 | // then 137 | XCTAssertNotNil(spy.perform___VARIABLE_sceneName___ViewModel.error?.message, "presentPerform___VARIABLE_sceneName___(with:) should return error message if there is an error") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Clean Swift/Scene.xctemplate/UIViewController/___FILEBASENAME___ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___DisplayLogic: class { 12 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) 13 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) 14 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) 15 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) 16 | } 17 | 18 | class ___VARIABLE_sceneName___ViewController: UIViewController, ___VARIABLE_sceneName___DisplayLogic { 19 | 20 | // MARK: - Properties 21 | 22 | typealias Models = ___VARIABLE_sceneName___Models 23 | var router: (NSObjectProtocol & ___VARIABLE_sceneName___RoutingLogic & ___VARIABLE_sceneName___DataPassing)? 24 | var interactor: ___VARIABLE_sceneName___BusinessLogic? 25 | 26 | // MARK: - Object lifecycle 27 | 28 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 29 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 30 | setup() 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | super.init(coder: aDecoder) 35 | setup() 36 | } 37 | 38 | // MARK: - Setup 39 | 40 | private func setup() { 41 | let viewController = self 42 | let interactor = ___VARIABLE_sceneName___Interactor() 43 | let presenter = ___VARIABLE_sceneName___Presenter() 44 | let router = ___VARIABLE_sceneName___Router() 45 | 46 | viewController.router = router 47 | viewController.interactor = interactor 48 | interactor.presenter = presenter 49 | presenter.viewController = viewController 50 | router.viewController = viewController 51 | router.dataStore = interactor 52 | } 53 | 54 | // MARK: - View Lifecycle 55 | 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | setupFetchFromLocalDataStore() 59 | } 60 | 61 | override func viewWillAppear(_ animated: Bool) { 62 | super.viewWillAppear(animated) 63 | setupFetchFromRemoteDataStore() 64 | } 65 | 66 | override func viewDidAppear(_ animated: Bool) { 67 | super.viewDidAppear(animated) 68 | trackScreenViewAnalytics() 69 | registerNotifications() 70 | } 71 | 72 | override func viewWillDisappear(_ animated: Bool) { 73 | super.viewWillDisappear(animated) 74 | unregisterNotifications() 75 | } 76 | 77 | // MARK: - Notifications 78 | 79 | func registerNotifications() { 80 | let selector = #selector(trackScreenViewAnalytics) 81 | let notification = UIApplication.didBecomeActiveNotification 82 | NotificationCenter.default.addObserver(self, selector: selector, name: notification, object: nil) 83 | } 84 | 85 | func unregisterNotifications() { 86 | NotificationCenter.default.removeObserver(self) 87 | } 88 | 89 | // MARK: - Use Case - Fetch From Local DataStore 90 | 91 | @IBOutlet var exampleLocalLabel: UILabel! = UILabel() 92 | func setupFetchFromLocalDataStore() { 93 | let request = Models.FetchFromLocalDataStore.Request() 94 | interactor?.fetchFromLocalDataStore(with: request) 95 | } 96 | 97 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) { 98 | exampleLocalLabel.text = viewModel.exampleTranslation 99 | } 100 | 101 | // MARK: - Use Case - Fetch From Remote DataStore 102 | 103 | @IBOutlet var exampleRemoteLabel: UILabel! = UILabel() 104 | func setupFetchFromRemoteDataStore() { 105 | let request = Models.FetchFromRemoteDataStore.Request() 106 | interactor?.fetchFromRemoteDataStore(with: request) 107 | } 108 | 109 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) { 110 | exampleRemoteLabel.text = viewModel.exampleVariable 111 | } 112 | 113 | // MARK: - Use Case - Track Analytics 114 | 115 | @objc 116 | func trackScreenViewAnalytics() { 117 | trackAnalytics(event: .screenView) 118 | } 119 | 120 | func trackAnalytics(event: ___VARIABLE_sceneName___Models.AnalyticsEvents) { 121 | let request = Models.TrackAnalytics.Request(event: event) 122 | interactor?.trackAnalytics(with: request) 123 | } 124 | 125 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) { 126 | // do something after tracking analytics (if needed) 127 | } 128 | 129 | // MARK: - Use Case - ___VARIABLE_sceneName___ 130 | 131 | func perform___VARIABLE_sceneName___(_ sender: Any) { 132 | let request = Models.Perform___VARIABLE_sceneName___.Request(exampleVariable: exampleLocalLabel.text) 133 | interactor?.perform___VARIABLE_sceneName___(with: request) 134 | } 135 | 136 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) { 137 | // handle error and ui element error states 138 | // based on error type 139 | if let error = viewModel.error { 140 | switch error.type { 141 | case .emptyExampleVariable: 142 | exampleLocalLabel.text = error.message 143 | 144 | case .networkError: 145 | exampleLocalLabel.text = error.message 146 | } 147 | 148 | return 149 | } 150 | 151 | // handle ui element success state and 152 | // route to next screen 153 | router?.routeToNext() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Clean Swift/View Controller.xctemplate/UIViewController/___FILEBASENAME___ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PROJECTNAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ___VARIABLE_sceneName___DisplayLogic: class { 12 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) 13 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) 14 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) 15 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) 16 | } 17 | 18 | class ___VARIABLE_sceneName___ViewController: UIViewController, ___VARIABLE_sceneName___DisplayLogic { 19 | 20 | // MARK: - Properties 21 | 22 | typealias Models = ___VARIABLE_sceneName___Models 23 | var router: (NSObjectProtocol & ___VARIABLE_sceneName___RoutingLogic & ___VARIABLE_sceneName___DataPassing)? 24 | var interactor: ___VARIABLE_sceneName___BusinessLogic? 25 | 26 | // MARK: - Object lifecycle 27 | 28 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 29 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 30 | setup() 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | super.init(coder: aDecoder) 35 | setup() 36 | } 37 | 38 | // MARK: - Setup 39 | 40 | private func setup() { 41 | let viewController = self 42 | let interactor = ___VARIABLE_sceneName___Interactor() 43 | let presenter = ___VARIABLE_sceneName___Presenter() 44 | let router = ___VARIABLE_sceneName___Router() 45 | 46 | viewController.router = router 47 | viewController.interactor = interactor 48 | interactor.presenter = presenter 49 | presenter.viewController = viewController 50 | router.viewController = viewController 51 | router.dataStore = interactor 52 | } 53 | 54 | // MARK: - View Lifecycle 55 | 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | setupFetchFromLocalDataStore() 59 | } 60 | 61 | override func viewWillAppear(_ animated: Bool) { 62 | super.viewWillAppear(animated) 63 | setupFetchFromRemoteDataStore() 64 | } 65 | 66 | override func viewDidAppear(_ animated: Bool) { 67 | super.viewDidAppear(animated) 68 | trackScreenViewAnalytics() 69 | registerNotifications() 70 | } 71 | 72 | override func viewWillDisappear(_ animated: Bool) { 73 | super.viewWillDisappear(animated) 74 | unregisterNotifications() 75 | } 76 | 77 | // MARK: - Notifications 78 | 79 | func registerNotifications() { 80 | let selector = #selector(trackScreenViewAnalytics) 81 | let notification = UIApplication.didBecomeActiveNotification 82 | NotificationCenter.default.addObserver(self, selector: selector, name: notification, object: nil) 83 | } 84 | 85 | func unregisterNotifications() { 86 | NotificationCenter.default.removeObserver(self) 87 | } 88 | 89 | // MARK: - Use Case - Fetch From Local DataStore 90 | 91 | @IBOutlet var exampleLocalLabel: UILabel! = UILabel() 92 | func setupFetchFromLocalDataStore() { 93 | let request = Models.FetchFromLocalDataStore.Request() 94 | interactor?.fetchFromLocalDataStore(with: request) 95 | } 96 | 97 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) { 98 | exampleLocalLabel.text = viewModel.exampleTranslation 99 | } 100 | 101 | // MARK: - Use Case - Fetch From Remote DataStore 102 | 103 | @IBOutlet var exampleRemoteLabel: UILabel! = UILabel() 104 | func setupFetchFromRemoteDataStore() { 105 | let request = Models.FetchFromRemoteDataStore.Request() 106 | interactor?.fetchFromRemoteDataStore(with: request) 107 | } 108 | 109 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) { 110 | exampleRemoteLabel.text = viewModel.exampleVariable 111 | } 112 | 113 | // MARK: - Use Case - Track Analytics 114 | 115 | @objc 116 | func trackScreenViewAnalytics() { 117 | trackAnalytics(event: .screenView) 118 | } 119 | 120 | func trackAnalytics(event: ___VARIABLE_sceneName___Models.AnalyticsEvents) { 121 | let request = Models.TrackAnalytics.Request(event: event) 122 | interactor?.trackAnalytics(with: request) 123 | } 124 | 125 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) { 126 | // do something after tracking analytics (if needed) 127 | } 128 | 129 | // MARK: - Use Case - ___VARIABLE_sceneName___ 130 | 131 | func perform___VARIABLE_sceneName___(_ sender: Any) { 132 | let request = Models.Perform___VARIABLE_sceneName___.Request(exampleVariable: exampleLocalLabel.text) 133 | interactor?.perform___VARIABLE_sceneName___(with: request) 134 | } 135 | 136 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) { 137 | // handle error and ui element error states 138 | // based on error type 139 | if let error = viewModel.error { 140 | switch error.type { 141 | case .emptyExampleVariable: 142 | exampleLocalLabel.text = error.message 143 | 144 | case .networkError: 145 | exampleLocalLabel.text = error.message 146 | } 147 | 148 | return 149 | } 150 | 151 | // handle ui element success state and 152 | // route to next screen 153 | router?.routeToNext() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/___FILEBASENAME___PresenterSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import ___PROJECTNAMEASIDENTIFIER___ 12 | 13 | class ___VARIABLE_sceneName___PresenterSpec: QuickSpec { 14 | override func spec() { 15 | 16 | // MARK: - Subject Under Test (SUT) 17 | 18 | typealias Models = NewGroupModels 19 | var sut: ___VARIABLE_sceneName___Presenter! 20 | 21 | // MARK: - Test Doubles 22 | 23 | var displayLogicSpy: ___VARIABLE_sceneName___DisplayLogicSpy! 24 | 25 | // MARK: - Tests 26 | 27 | beforeEach { 28 | setupInitialUserState() 29 | setupPresenter() 30 | setupDisplayLogic() 31 | } 32 | 33 | afterEach { 34 | sut = nil 35 | displayLogicSpy = nil 36 | } 37 | 38 | // MARK: - Use Cases 39 | 40 | describe("present fetch from local data store") { 41 | it("should ask view controller to display", closure: { 42 | // given 43 | let response = Models.FetchFromLocalDataStore.Response() 44 | 45 | // when 46 | sut.presentFetchFromLocalDataStore(with: response) 47 | 48 | // then 49 | expect(displayLogicSpy.displayFetchFromLocalDataStoreCalled).to(beTrue()) 50 | }) 51 | } 52 | 53 | describe("present fetch from remote data store") { 54 | it("should ask view controller to display", closure: { 55 | // given 56 | let response = Models.FetchFromRemoteDataStore.Response() 57 | 58 | // when 59 | sut.presentFetchFromRemoteDataStore(with: response) 60 | 61 | // then 62 | expect(displayLogicSpy.displayFetchFromRemoteDataStoreCalled).to(beTrue()) 63 | }) 64 | } 65 | 66 | describe("present track analytics") { 67 | it("should ask view controller to display", closure: { 68 | // given 69 | let response = Models.TrackAnalytics.Response() 70 | 71 | // when 72 | sut.presentTrackAnalytics(with: response) 73 | 74 | // then 75 | expect(displayLogicSpy.displayTrackAnalyticsCalled).to(beTrue()) 76 | }) 77 | } 78 | 79 | describe("present perform ___VARIABLE_sceneName___") { 80 | context("when there are error(s)", closure: { 81 | it("should return error message", closure: { 82 | // given 83 | let error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 84 | let response = Models.Perform___VARIABLE_sceneName___.Response(error: error) 85 | 86 | // when 87 | sut.presentPerform___VARIABLE_sceneName___(with: response) 88 | 89 | // then 90 | expect(displayLogicSpy.perform___VARIABLE_sceneName___ViewModel.error?.message).notTo(beNil()) 91 | }) 92 | }) 93 | 94 | it("should ask view controller to display", closure: { 95 | // given 96 | let response = Models.Perform___VARIABLE_sceneName___.Response() 97 | 98 | // when 99 | sut.presentPerform___VARIABLE_sceneName___(with: response) 100 | 101 | // then 102 | expect(displayLogicSpy.displayPerform___VARIABLE_sceneName___Called).to(beTrue()) 103 | }) 104 | } 105 | 106 | // MARK: - Test Helpers 107 | 108 | func setupInitialUserState() { 109 | // some initial user state setup 110 | } 111 | 112 | func setupPresenter() { 113 | sut = ___VARIABLE_sceneName___Presenter() 114 | } 115 | 116 | func setupDisplayLogic() { 117 | displayLogicSpy = ___VARIABLE_sceneName___DisplayLogicSpy() 118 | sut.viewController = displayLogicSpy 119 | } 120 | } 121 | } 122 | 123 | // MARK: - Test Doubles 124 | 125 | extension ___VARIABLE_sceneName___PresenterSpec { 126 | class ___VARIABLE_sceneName___DisplayLogicSpy: ___VARIABLE_sceneName___DisplayLogic { 127 | 128 | // MARK: Spied Methods 129 | 130 | var displayFetchFromLocalDataStoreCalled = false 131 | var fetchFromLocalDataStoreViewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel! 132 | func displayFetchFromLocalDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.ViewModel) { 133 | displayFetchFromLocalDataStoreCalled = true 134 | fetchFromLocalDataStoreViewModel = viewModel 135 | } 136 | 137 | var displayFetchFromRemoteDataStoreCalled = false 138 | var fetchFromRemoteDataStoreViewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel! 139 | func displayFetchFromRemoteDataStore(with viewModel: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.ViewModel) { 140 | displayFetchFromRemoteDataStoreCalled = true 141 | fetchFromRemoteDataStoreViewModel = viewModel 142 | } 143 | 144 | var displayTrackAnalyticsCalled = false 145 | var trackAnalyticsViewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel! 146 | func displayTrackAnalytics(with viewModel: ___VARIABLE_sceneName___Models.TrackAnalytics.ViewModel) { 147 | displayTrackAnalyticsCalled = true 148 | trackAnalyticsViewModel = viewModel 149 | } 150 | 151 | var displayPerform___VARIABLE_sceneName___Called = false 152 | var perform___VARIABLE_sceneName___ViewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel! 153 | func displayPerform___VARIABLE_sceneName___(with viewModel: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel) { 154 | displayPerform___VARIABLE_sceneName___Called = true 155 | perform___VARIABLE_sceneName___ViewModel = viewModel 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/___FILEBASENAME___InteractorSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import ___PROJECTNAMEASIDENTIFIER___ 12 | 13 | class ___VARIABLE_sceneName___InteractorSpec: QuickSpec { 14 | override func spec() { 15 | 16 | // MARK: - Subject Under Test (SUT) 17 | 18 | typealias Models = ___VARIABLE_sceneName___Models 19 | var sut: ___VARIABLE_sceneName___Interactor! 20 | 21 | // MARK: - Test Doubles 22 | 23 | var presentationLogicSpy: ___VARIABLE_sceneName___PresentationLogicSpy! 24 | var workerSpy: ___VARIABLE_sceneName___WorkerSpy! 25 | 26 | // MARK: - Tests 27 | 28 | beforeEach { 29 | setupInitialUserState() 30 | setupInteractor() 31 | setupPresentationLogic() 32 | setupWorker() 33 | } 34 | 35 | afterEach { 36 | sut = nil 37 | presentationLogicSpy = nil 38 | workerSpy = nil 39 | } 40 | 41 | // MARK: - Use Cases 42 | 43 | describe("fetch from local data store") { 44 | it("should ask presenter to format", closure: { 45 | // given 46 | let request = Models.FetchFromLocalDataStore.Request() 47 | 48 | // when 49 | sut.fetchFromLocalDataStore(with: request) 50 | 51 | // then 52 | expect(presentationLogicSpy.presentFetchFromLocalDataStoreCalled).to(beTrue()) 53 | }) 54 | } 55 | 56 | describe("fetch from remote data store") { 57 | beforeEach { 58 | // given 59 | let request = Models.FetchFromRemoteDataStore.Request() 60 | 61 | // when 62 | sut.fetchFromRemoteDataStore(with: request) 63 | } 64 | 65 | it("should ask presenter to format", closure: { 66 | // then 67 | // expect(presentationLogicSpy.presentFetchFromRemoteDataStoreCalled).toEventually(beTrue()) 68 | }) 69 | } 70 | 71 | describe("track analytics") { 72 | beforeEach { 73 | // given 74 | let request = Models.TrackAnalytics.Request(event: .screenView) 75 | 76 | // when 77 | sut.trackAnalytics(with: request) 78 | } 79 | 80 | it("should ask presenter to format", closure: { 81 | // then 82 | expect(presentationLogicSpy.presentTrackAnalyticsCalled).to(beTrue()) 83 | }) 84 | } 85 | 86 | describe("perform ___VARIABLE_sceneName___") { 87 | it("should validate example variable", closure: { 88 | // given 89 | let request = Models.Perform___VARIABLE_sceneName___.Request() 90 | 91 | // when 92 | sut.perform___VARIABLE_sceneName___(with: request) 93 | 94 | // then 95 | expect(workerSpy.validateExampleVariableCalled).to(beTrue()) 96 | }) 97 | 98 | it("should ask presenter to format", closure: { 99 | // given 100 | let request = Models.Perform___VARIABLE_sceneName___.Request() 101 | 102 | // when 103 | sut.perform___VARIABLE_sceneName___(with: request) 104 | 105 | // then 106 | expect(presentationLogicSpy.presentPerform___VARIABLE_sceneName___Called).toEventually(beTrue()) 107 | }) 108 | } 109 | 110 | // MARK: - Test Helpers 111 | 112 | func setupInitialUserState() { 113 | // some initial user state setup 114 | } 115 | 116 | func setupInteractor() { 117 | sut = ___VARIABLE_sceneName___Interactor() 118 | } 119 | 120 | func setupPresentationLogic() { 121 | presentationLogicSpy = ___VARIABLE_sceneName___PresentationLogicSpy() 122 | sut.presenter = presentationLogicSpy 123 | } 124 | 125 | func setupWorker() { 126 | workerSpy = ___VARIABLE_sceneName___WorkerSpy() 127 | sut.worker = workerSpy 128 | } 129 | } 130 | } 131 | 132 | // MARK: - Test Doubles 133 | 134 | extension ___VARIABLE_sceneName___InteractorSpec { 135 | class ___VARIABLE_sceneName___PresentationLogicSpy: ___VARIABLE_sceneName___PresentationLogic { 136 | 137 | // MARK: Spied Methods 138 | 139 | var presentFetchFromLocalDataStoreCalled = false 140 | var fetchFromLocalDataStoreResponse: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response! 141 | func presentFetchFromLocalDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Response) { 142 | presentFetchFromLocalDataStoreCalled = true 143 | fetchFromLocalDataStoreResponse = response 144 | } 145 | 146 | var presentFetchFromRemoteDataStoreCalled = false 147 | var fetchFromRemoteDataStoreResponse: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response! 148 | func presentFetchFromRemoteDataStore(with response: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Response) { 149 | presentFetchFromRemoteDataStoreCalled = true 150 | fetchFromRemoteDataStoreResponse = response 151 | } 152 | 153 | var presentTrackAnalyticsCalled = false 154 | var trackAnalyticsResponse: ___VARIABLE_sceneName___Models.TrackAnalytics.Response! 155 | func presentTrackAnalytics(with response: ___VARIABLE_sceneName___Models.TrackAnalytics.Response) { 156 | presentTrackAnalyticsCalled = true 157 | trackAnalyticsResponse = response 158 | } 159 | 160 | var presentPerform___VARIABLE_sceneName___Called = false 161 | var perform___VARIABLE_sceneName___Response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response! 162 | func presentPerform___VARIABLE_sceneName___(with response: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Response) { 163 | presentPerform___VARIABLE_sceneName___Called = true 164 | perform___VARIABLE_sceneName___Response = response 165 | } 166 | } 167 | 168 | class ___VARIABLE_sceneName___WorkerSpy: ___VARIABLE_sceneName___Worker { 169 | 170 | // MARK: Spied Methods 171 | 172 | var validateExampleVariableCalled = false 173 | override func validate(exampleVariable: String?) -> ___VARIABLE_sceneName___Worker.Models.___VARIABLE_sceneName___Error? { 174 | validateExampleVariableCalled = true 175 | return super.validate(exampleVariable: exampleVariable) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (XCTest).xctemplate/___FILEBASENAME___ViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | @testable import ___PROJECTNAMEASIDENTIFIER___ 10 | import XCTest 11 | 12 | class ___VARIABLE_sceneName___ViewControllerTests: XCTestCase { 13 | 14 | // MARK: - Subject Under Test (SUT) 15 | 16 | typealias Models = ___VARIABLE_sceneName___Models 17 | var sut: ___VARIABLE_sceneName___ViewController! 18 | var window: UIWindow! 19 | 20 | // MARK: - Test Lifecycle 21 | 22 | override func setUp() { 23 | super.setUp() 24 | window = UIWindow() 25 | setup___VARIABLE_sceneName___ViewController() 26 | } 27 | 28 | override func tearDown() { 29 | window = nil 30 | sut = nil 31 | super.tearDown() 32 | } 33 | 34 | // MARK: - Test Setup 35 | 36 | func setup___VARIABLE_sceneName___ViewController() { 37 | let bundle = Bundle.main 38 | let storyboard = UIStoryboard(name: "Main", bundle: bundle) 39 | sut = storyboard.instantiateViewController(withIdentifier: "___VARIABLE_sceneName___ViewController") as? ___VARIABLE_sceneName___ViewController 40 | } 41 | 42 | func loadView() { 43 | window.addSubview(sut.view) 44 | RunLoop.current.run(until: Date()) 45 | } 46 | 47 | // MARK: - Test Doubles 48 | 49 | class ___VARIABLE_sceneName___BusinessLogicSpy: ___VARIABLE_sceneName___BusinessLogic { 50 | 51 | // MARK: Spied Methods 52 | 53 | var fetchFromLocalDataStoreCalled = false 54 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) { 55 | fetchFromLocalDataStoreCalled = true 56 | } 57 | 58 | var fetchFromRemoteDataStoreCalled = false 59 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) { 60 | fetchFromRemoteDataStoreCalled = true 61 | } 62 | 63 | var trackAnalyticsCalled = false 64 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) { 65 | trackAnalyticsCalled = true 66 | } 67 | 68 | var perform___VARIABLE_sceneName___Called = false 69 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) { 70 | perform___VARIABLE_sceneName___Called = true 71 | } 72 | } 73 | 74 | class ___VARIABLE_sceneName___RouterSpy: ___VARIABLE_sceneName___Router { 75 | 76 | // MARK: Spied Methods 77 | 78 | var routeToNextCalled = false 79 | override func routeToNext() { 80 | routeToNextCalled = true 81 | } 82 | } 83 | 84 | // MARK: - Tests 85 | 86 | func testShouldFetchFromLocalDataStoreWhenViewIsLoaded() { 87 | // given 88 | let spy = ___VARIABLE_sceneName___BusinessLogicSpy() 89 | sut.interactor = spy 90 | 91 | // when 92 | loadView() 93 | 94 | // then 95 | XCTAssertTrue(spy.fetchFromLocalDataStoreCalled, "viewDidLoad() should ask the interactor to fetch from local DataStore") 96 | } 97 | 98 | func testShouldDisplayDataFetchedFromLocalDataStore() { 99 | // given 100 | loadView() 101 | let translation = "Example string." 102 | let viewModel = Models.FetchFromLocalDataStore.ViewModel(exampleTranslation: translation) 103 | 104 | // when 105 | sut.displayFetchFromLocalDataStore(with: viewModel) 106 | 107 | // then 108 | XCTAssertEqual(sut.exampleLocalLabel.text, translation, "displayFetchFromLocalDataStore(with:) should display the correct example label text") 109 | } 110 | 111 | func testShouldFetchFromRemoteDataStoreWhenViewWillAppear() { 112 | // given 113 | let spy = ___VARIABLE_sceneName___BusinessLogicSpy() 114 | sut.interactor = spy 115 | 116 | // when 117 | loadView() 118 | 119 | // then 120 | XCTAssertTrue(spy.fetchFromRemoteDataStoreCalled, "viewWillAppear(_:) should ask the interactor to fetch from remote DataStore") 121 | } 122 | 123 | func testShouldDisplayDataFetchedFromRemoteDataStore() { 124 | // given 125 | loadView() 126 | let exampleVariable = "Example string." 127 | let viewModel = Models.FetchFromRemoteDataStore.ViewModel(exampleVariable: exampleVariable) 128 | 129 | // when 130 | sut.displayFetchFromRemoteDataStore(with: viewModel) 131 | 132 | // then 133 | XCTAssertEqual(sut.exampleRemoteLabel.text, exampleVariable, "displayFetchFromRemoteDataStore(with:) should display the correct example label text") 134 | } 135 | 136 | func testShouldTrackAnalyticsWhenViewDidAppear() { 137 | // given 138 | let spy = ___VARIABLE_sceneName___BusinessLogicSpy() 139 | sut.interactor = spy 140 | loadView() 141 | 142 | // when 143 | sut.viewDidAppear(true) 144 | 145 | // then 146 | XCTAssertTrue(spy.trackAnalyticsCalled, "When needed, view controller should ask the interactor to track analytics") 147 | } 148 | 149 | func testShouldDisplayTrackAnalyticsWhenDisplayTrackAnalytics() { 150 | // given 151 | loadView() 152 | let viewModel = Models.TrackAnalytics.ViewModel() 153 | 154 | // when 155 | sut.displayTrackAnalytics(with: viewModel) 156 | 157 | // then 158 | // assert something here based on use case 159 | } 160 | 161 | func testUnsuccessful___VARIABLE_sceneName___ShouldShowErrorAsLabel() { 162 | // given 163 | loadView() 164 | var error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 165 | error.message = "Example error" 166 | let viewModel = Models.Perform___VARIABLE_sceneName___.ViewModel(error: error) 167 | 168 | // when 169 | sut.displayPerform___VARIABLE_sceneName___(with: viewModel) 170 | 171 | // then 172 | XCTAssertEqual(sut.exampleLocalLabel.text, error.message, "displayPerform___VARIABLE_sceneName___(with:) should set error as label if there is an error") 173 | } 174 | 175 | func testUnsuccessful___VARIABLE_sceneName___ShouldNotRouteToNext() { 176 | // given 177 | let spy = ___VARIABLE_sceneName___RouterSpy() 178 | sut.router = spy 179 | loadView() 180 | let error = ___VARIABLE_sceneName___Models.Error<___VARIABLE_sceneName___Models.___VARIABLE_sceneName___ErrorType>.init(type: .emptyExampleVariable) 181 | let viewModel = ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel(error: error) 182 | 183 | // when 184 | sut.displayPerform___VARIABLE_sceneName___(with: viewModel) 185 | 186 | // then 187 | XCTAssertFalse(spy.routeToNextCalled, "displayPerform___VARIABLE_sceneName___(with:) should not route to next screen if there is an error") 188 | } 189 | 190 | func testSuccessful___VARIABLE_sceneName___ShouldRouteToNext() { 191 | // given 192 | let spy = ___VARIABLE_sceneName___RouterSpy() 193 | sut.router = spy 194 | loadView() 195 | let viewModel = ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.ViewModel(error: nil) 196 | 197 | // when 198 | sut.displayPerform___VARIABLE_sceneName___(with: viewModel) 199 | 200 | // then 201 | XCTAssertTrue(spy.routeToNextCalled, "displayPerform___VARIABLE_sceneName___(with:) should route to next screen if there is no error") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Clean Swift/Unit Tests (Quick-Nimble).xctemplate/___FILEBASENAME___ViewControllerSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ___FILENAME___ 3 | // ___PACKAGENAME___ 4 | // 5 | // Created by ___FULLUSERNAME___ on ___DATE___. 6 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | @testable import ___PROJECTNAMEASIDENTIFIER___ 12 | 13 | class ___VARIABLE_sceneName___ViewControllerSpec: QuickSpec { 14 | override func spec() { 15 | 16 | // MARK: - Subject Under Test (SUT) 17 | 18 | typealias Models = ___VARIABLE_sceneName___Models 19 | var sut: ___VARIABLE_sceneName___ViewController! 20 | var window: UIWindow! 21 | 22 | // MARK: - Test Doubles 23 | 24 | var businessLogicSpy: ___VARIABLE_sceneName___BusinessLogicSpy! 25 | var routerSpy: ___VARIABLE_sceneName___RouterSpy! 26 | 27 | // MARK: - Tests 28 | 29 | beforeEach { 30 | window = UIWindow() 31 | setupInitialUserState() 32 | setupViewController() 33 | setupBusinessLogic() 34 | setupRouter() 35 | } 36 | 37 | afterEach { 38 | window = nil 39 | sut = nil 40 | businessLogicSpy = nil 41 | routerSpy = nil 42 | } 43 | 44 | // MARK: - View Lifecycle 45 | 46 | describe("view did load") { 47 | it("should fetch from local datastore", closure: { 48 | // given 49 | 50 | // when 51 | loadView() 52 | 53 | // then 54 | expect(businessLogicSpy.fetchFromLocalDataStoreCalled).to(beTrue()) 55 | }) 56 | } 57 | 58 | describe("view will appear") { 59 | it("should fetch from remote datastore", closure: { 60 | // given 61 | loadView() 62 | 63 | // when 64 | sut.viewWillAppear(true) 65 | 66 | // then 67 | expect(businessLogicSpy.fetchFromRemoteDataStoreCalled).to(beTrue()) 68 | }) 69 | } 70 | 71 | describe("view did appear") { 72 | it("should track analytics", closure: { 73 | // given 74 | loadView() 75 | 76 | // when 77 | sut.viewDidAppear(true) 78 | 79 | // then 80 | expect(businessLogicSpy.trackAnalyticsCalled).to(beTrue()) 81 | }) 82 | } 83 | 84 | // MARK: - IBActions/Delegates 85 | 86 | // MARK: - Display Logic 87 | 88 | describe("display fetch from local datastore") { 89 | it("should display fetch from local datastore", closure: { 90 | // given 91 | loadView() 92 | let translation = "Example string." 93 | let viewModel = Models.FetchFromLocalDataStore.ViewModel(exampleTranslation: translation) 94 | 95 | // when 96 | sut.displayFetchFromLocalDataStore(with: viewModel) 97 | 98 | // then 99 | expect(sut.exampleLocalLabel.text).to(equal(translation)) 100 | }) 101 | } 102 | 103 | describe("display fetch from remote datastore") { 104 | it("should display fetch from remote datastore", closure: { 105 | // given 106 | loadView() 107 | let exampleVariable = "Example string." 108 | let viewModel = Models.FetchFromRemoteDataStore.ViewModel(exampleVariable: exampleVariable) 109 | 110 | // when 111 | sut.displayFetchFromRemoteDataStore(with: viewModel) 112 | 113 | // then 114 | expect(sut.exampleRemoteLabel.text).to(equal(exampleVariable)) 115 | }) 116 | } 117 | 118 | describe("display track analytics") { 119 | it("should display track analytics", closure: { 120 | // given 121 | loadView() 122 | let viewModel = Models.TrackAnalytics.ViewModel() 123 | 124 | // when 125 | sut.displayTrackAnalytics(with: viewModel) 126 | 127 | // then 128 | // assert something here based on use case 129 | }) 130 | } 131 | 132 | describe("display perform ___VARIABLE_sceneName___") { 133 | context("when there is an error", closure: { 134 | var error: Models.___VARIABLE_sceneName___Error! 135 | 136 | beforeEach { 137 | // given 138 | loadView() 139 | error = Models.___VARIABLE_sceneName___Error(type: .emptyExampleVariable) 140 | error.message = "Example error" 141 | let viewModel = Models.Perform___VARIABLE_sceneName___.ViewModel(error: error) 142 | 143 | // when 144 | sut.displayPerform___VARIABLE_sceneName___(with: viewModel) 145 | } 146 | 147 | it("should show error as label", closure: { 148 | // then 149 | expect(sut.exampleLocalLabel.text).to(equal(error.message)) 150 | }) 151 | 152 | it("should not route to next", closure: { 153 | // then 154 | expect(routerSpy.routeToNextCalled).to(beFalse()) 155 | }) 156 | }) 157 | 158 | context("when there is no error", closure: { 159 | it("should route to next", closure: { 160 | // given 161 | loadView() 162 | let viewModel = Models.Perform___VARIABLE_sceneName___.ViewModel(error: nil) 163 | 164 | // when 165 | sut.displayPerform___VARIABLE_sceneName___(with: viewModel) 166 | 167 | // then 168 | expect(routerSpy.routeToNextCalled).to(beTrue()) 169 | }) 170 | }) 171 | } 172 | 173 | // MARK: - Test Helpers 174 | 175 | func setupInitialUserState() { 176 | // some initial user state setup 177 | } 178 | 179 | func setupViewController() { 180 | let bundle = Bundle.main 181 | let storyboard = UIStoryboard(name: "Main", bundle: bundle) 182 | sut = storyboard.instantiateViewController(withIdentifier: "___VARIABLE_sceneName___ViewController") as? ___VARIABLE_sceneName___ViewController 183 | } 184 | 185 | func setupBusinessLogic() { 186 | businessLogicSpy = ___VARIABLE_sceneName___BusinessLogicSpy() 187 | sut.interactor = businessLogicSpy 188 | } 189 | 190 | func setupRouter() { 191 | routerSpy = ___VARIABLE_sceneName___RouterSpy() 192 | sut.router = routerSpy 193 | } 194 | 195 | func loadView() { 196 | window.addSubview(sut.view) 197 | RunLoop.current.run(until: Date()) 198 | } 199 | } 200 | } 201 | 202 | // MARK: - Test Doubles 203 | 204 | extension ___VARIABLE_sceneName___ViewControllerSpec { 205 | class ___VARIABLE_sceneName___BusinessLogicSpy: ___VARIABLE_sceneName___BusinessLogic { 206 | 207 | // MARK: Spied Methods 208 | 209 | var fetchFromLocalDataStoreCalled = false 210 | var fetchFromLocalDataStoreRequest: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request! 211 | func fetchFromLocalDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromLocalDataStore.Request) { 212 | fetchFromLocalDataStoreCalled = true 213 | fetchFromLocalDataStoreRequest = request 214 | } 215 | 216 | var fetchFromRemoteDataStoreCalled = false 217 | var fetchFromRemoteDataStoreRequest: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request! 218 | func fetchFromRemoteDataStore(with request: ___VARIABLE_sceneName___Models.FetchFromRemoteDataStore.Request) { 219 | fetchFromRemoteDataStoreCalled = true 220 | fetchFromRemoteDataStoreRequest = request 221 | } 222 | 223 | var trackAnalyticsCalled = false 224 | var trackAnalyticsRequest: ___VARIABLE_sceneName___Models.TrackAnalytics.Request! 225 | func trackAnalytics(with request: ___VARIABLE_sceneName___Models.TrackAnalytics.Request) { 226 | trackAnalyticsCalled = true 227 | trackAnalyticsRequest = request 228 | } 229 | 230 | var perform___VARIABLE_sceneName___Called = false 231 | var ___VARIABLE_sceneName___Request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request! 232 | func perform___VARIABLE_sceneName___(with request: ___VARIABLE_sceneName___Models.Perform___VARIABLE_sceneName___.Request) { 233 | perform___VARIABLE_sceneName___Called = true 234 | ___VARIABLE_sceneName___Request = request 235 | } 236 | } 237 | 238 | class ___VARIABLE_sceneName___RouterSpy: ___VARIABLE_sceneName___Router { 239 | 240 | // MARK: Spied Methods 241 | 242 | var routeToNextCalled = false 243 | override func routeToNext() { 244 | routeToNextCalled = true 245 | } 246 | } 247 | } 248 | --------------------------------------------------------------------------------