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