├── Images
├── VIP.png
├── Thread.png
├── UseCase.png
├── Worker.png
├── ASIS-Test.png
├── Mutation.png
├── TOBE-Test.png
├── Templates.png
├── DomainState.png
├── Flow-Example.gif
├── Main-Diagram.png
├── Communication.png
└── iOS-World-CleanSwift.png
├── Example
├── Example
│ ├── Scene
│ │ ├── AddRequestsScene
│ │ │ ├── README.md
│ │ │ ├── Sources
│ │ │ │ └── AddRequestsScene
│ │ │ │ │ ├── AddRequestsModels.swift
│ │ │ │ │ ├── AddRequestsWorker.swift
│ │ │ │ │ ├── AddRequestsController.swift
│ │ │ │ │ ├── AddRequestsInteractor.swift
│ │ │ │ │ ├── View
│ │ │ │ │ └── Any
│ │ │ │ │ │ ├── AddRequestsSwiftUIView.swift
│ │ │ │ │ │ └── AddRequestsUIKitView.swift
│ │ │ │ │ └── AddRequestsStore.swift
│ │ │ ├── Package.swift
│ │ │ └── Tests
│ │ │ │ └── AddRequestsSceneTests
│ │ │ │ ├── Unit
│ │ │ │ ├── AddRequestsControllerTests.swift
│ │ │ │ ├── AddRequestsStoreTests.swift
│ │ │ │ └── AddRequestsInteractorTests.swift
│ │ │ │ └── AddRequestsSceneTests.swift
│ │ └── UploadReceiptScene
│ │ │ ├── README.md
│ │ │ ├── .swiftpm
│ │ │ └── xcode
│ │ │ │ └── package.xcworkspace
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── Package.swift
│ │ │ ├── Sources
│ │ │ └── UploadReceiptScene
│ │ │ │ ├── UploadReceiptModels.swift
│ │ │ │ ├── UploadReceiptController.swift
│ │ │ │ ├── UploadReceiptWorker.swift
│ │ │ │ ├── UploadReceiptInteractor.swift
│ │ │ │ ├── UploadReceiptStore.swift
│ │ │ │ └── View
│ │ │ │ └── Any
│ │ │ │ ├── UploadReceiptSwiftUIView.swift
│ │ │ │ └── UploadReceiptUIKitView.swift
│ │ │ └── Tests
│ │ │ └── UploadReceiptSceneTests
│ │ │ └── Unit
│ │ │ ├── UploadReceiptControllerTests.swift
│ │ │ └── UploadReceiptStoreTests.swift
│ ├── Service
│ │ └── NetworkService
│ │ │ ├── README.md
│ │ │ ├── .gitignore
│ │ │ ├── .swiftpm
│ │ │ └── xcode
│ │ │ │ └── package.xcworkspace
│ │ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ ├── Sources
│ │ │ └── NetworkService
│ │ │ │ ├── DTO
│ │ │ │ └── UploadUrlsDTO.swift
│ │ │ │ ├── QuotationNetworkService.swift
│ │ │ │ └── ImageUploadNetworkService.swift
│ │ │ ├── Tests
│ │ │ └── NetworkServiceTests
│ │ │ │ └── NetworkServiceTests.swift
│ │ │ └── Package.swift
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── ExampleAppWrapper.swift
│ ├── Info.plist
│ └── SceneDelegate.swift
└── Example.xcodeproj
│ └── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── Templates
├── Clean Swift - CIS
│ ├── Scene-Package.xctemplate
│ │ ├── SwiftUI
│ │ │ └── ___FILEBASENAME___Scene
│ │ │ │ ├── README.md
│ │ │ │ ├── .swiftpm
│ │ │ │ └── xcode
│ │ │ │ │ └── package.xcworkspace
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ ├── Sources
│ │ │ │ └── ___FILEBASENAME___Scene
│ │ │ │ │ ├── ___FILEBASENAME___Models.swift
│ │ │ │ │ ├── ___FILEBASENAME___Worker.swift
│ │ │ │ │ ├── ___FILEBASENAME___View.swift
│ │ │ │ │ ├── ___FILEBASENAME___Interactor.swift
│ │ │ │ │ ├── ___FILEBASENAME___Controller.swift
│ │ │ │ │ └── ___FILEBASENAME___Store.swift
│ │ │ │ ├── Package.swift
│ │ │ │ └── Tests
│ │ │ │ └── ___FILEBASENAME___SceneTests
│ │ │ │ ├── Unit
│ │ │ │ ├── ___FILEBASENAME___StoreTests.swift
│ │ │ │ ├── ___FILEBASENAME___InteractorTests.swift
│ │ │ │ └── ___FILEBASENAME___ControllerTests.swift
│ │ │ │ └── ___FILEBASENAME___SceneTests.swift
│ │ ├── UIKit
│ │ │ └── ___FILEBASENAME___Scene
│ │ │ │ ├── README.md
│ │ │ │ ├── .swiftpm
│ │ │ │ └── xcode
│ │ │ │ │ └── package.xcworkspace
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ ├── Sources
│ │ │ │ └── ___FILEBASENAME___Scene
│ │ │ │ │ ├── ___FILEBASENAME___Models.swift
│ │ │ │ │ ├── ___FILEBASENAME___Worker.swift
│ │ │ │ │ ├── ___FILEBASENAME___Interactor.swift
│ │ │ │ │ ├── ___FILEBASENAME___Controller.swift
│ │ │ │ │ ├── ___FILEBASENAME___View.swift
│ │ │ │ │ └── ___FILEBASENAME___Store.swift
│ │ │ │ ├── Package.swift
│ │ │ │ └── Tests
│ │ │ │ └── ___FILEBASENAME___SceneTests
│ │ │ │ ├── Unit
│ │ │ │ ├── ___FILEBASENAME___StoreTests.swift
│ │ │ │ ├── ___FILEBASENAME___InteractorTests.swift
│ │ │ │ └── ___FILEBASENAME___ControllerTests.swift
│ │ │ │ └── ___FILEBASENAME___SceneTests.swift
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ └── TemplateInfo.plist
│ ├── Scene.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── UIKit
│ │ │ ├── ___FILEBASENAME___Models.swift
│ │ │ ├── ___FILEBASENAME___Worker.swift
│ │ │ ├── ___FILEBASENAME___Interactor.swift
│ │ │ ├── ___FILEBASENAME___Controller.swift
│ │ │ ├── ___FILEBASENAME___View.swift
│ │ │ └── ___FILEBASENAME___Store.swift
│ │ ├── SwiftUI
│ │ │ ├── ___FILEBASENAME___Models.swift
│ │ │ ├── ___FILEBASENAME___Worker.swift
│ │ │ ├── ___FILEBASENAME___View.swift
│ │ │ ├── ___FILEBASENAME___Interactor.swift
│ │ │ ├── ___FILEBASENAME___Controller.swift
│ │ │ └── ___FILEBASENAME___Store.swift
│ │ └── TemplateInfo.plist
│ ├── Store.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── TemplateInfo.plist
│ │ └── ___FILEBASENAME___Store.swift
│ ├── View.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── SwiftUI
│ │ │ └── ___FILEBASENAME___View.swift
│ │ ├── TemplateInfo.plist
│ │ └── UIKit
│ │ │ └── ___FILEBASENAME___View.swift
│ ├── Worker.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── Default
│ │ │ └── ___FILEBASENAME___Worker.swift
│ │ ├── Delegate
│ │ │ └── ___FILEBASENAME___Worker.swift
│ │ └── TemplateInfo.plist
│ ├── Controller.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── TemplateInfo.plist
│ │ └── ___FILEBASENAME___Controller.swift
│ ├── Interactor.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── TemplateInfo.plist
│ │ └── ___FILEBASENAME___Interactor.swift
│ ├── SceneTests.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── TemplateInfo.plist
│ │ └── ___FILEBASENAME___SceneTests.swift
│ └── UnitTests.xctemplate
│ │ ├── TemplateIcon.png
│ │ ├── TemplateIcon@2x.png
│ │ ├── ___FILEBASENAME___StoreTests.swift
│ │ ├── ___FILEBASENAME___InteractorTests.swift
│ │ ├── ___FILEBASENAME___ControllerTests.swift
│ │ └── TemplateInfo.plist
└── Makefile
├── README.md
├── LICENSE
└── .gitignore
/Images/VIP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/VIP.png
--------------------------------------------------------------------------------
/Images/Thread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Thread.png
--------------------------------------------------------------------------------
/Images/UseCase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/UseCase.png
--------------------------------------------------------------------------------
/Images/Worker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Worker.png
--------------------------------------------------------------------------------
/Images/ASIS-Test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/ASIS-Test.png
--------------------------------------------------------------------------------
/Images/Mutation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Mutation.png
--------------------------------------------------------------------------------
/Images/TOBE-Test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/TOBE-Test.png
--------------------------------------------------------------------------------
/Images/Templates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Templates.png
--------------------------------------------------------------------------------
/Images/DomainState.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/DomainState.png
--------------------------------------------------------------------------------
/Images/Flow-Example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Flow-Example.gif
--------------------------------------------------------------------------------
/Images/Main-Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Main-Diagram.png
--------------------------------------------------------------------------------
/Images/Communication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/Communication.png
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/README.md:
--------------------------------------------------------------------------------
1 | # AddRequestsScene
2 |
3 | A description of this scene.
4 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/README.md:
--------------------------------------------------------------------------------
1 | # UploadReceiptScene
2 |
3 | A description of this scene.
4 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/README.md:
--------------------------------------------------------------------------------
1 | # NetworkService
2 |
3 | A description of this package.
4 |
--------------------------------------------------------------------------------
/Images/iOS-World-CleanSwift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Images/iOS-World-CleanSwift.png
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/README.md:
--------------------------------------------------------------------------------
1 | # ___VARIABLE_sceneName___Scene
2 |
3 | A description of this scene.
4 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/README.md:
--------------------------------------------------------------------------------
1 | # ___VARIABLE_sceneName___Scene
2 |
3 | A description of this scene.
4 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Scene.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Store.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Store.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/View.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/View.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/View.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/View.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Worker.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Worker.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Controller.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Controller.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Interactor.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Interactor.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Scene.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/SceneTests.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/SceneTests.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Store.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Store.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/UnitTests.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Worker.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Worker.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/UnitTests.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Controller.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Controller.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Interactor.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Interactor.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/TemplateIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Scene-Package.xctemplate/TemplateIcon.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/SceneTests.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/SceneTests.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/TemplateIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spoqa/CleanSwift-CIS/HEAD/Templates/Clean Swift - CIS/Scene-Package.xctemplate/TemplateIcon@2x.png
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/ExampleAppWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleApp.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ExampleAppWrapper {
12 | static func main() {
13 | UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(SceneDelegate.self))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "toaster",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/devxoul/Toaster.git",
7 | "state" : {
8 | "branch" : "master",
9 | "revision" : "a5aacb802cc6c243c3283899529a11f7b0f2ab53"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/Sources/NetworkService/DTO/UploadUrlsDTO.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadUrlsDTO.swift
3 | //
4 | //
5 | // Created by 박건우 on 2023/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct UploadUrlsDTO: Codable {
11 | public let uploadUrls: [UploadUrlDTO]
12 | }
13 |
14 | public struct UploadUrlDTO: Codable {
15 | public let uploadUrl: String
16 | public let objectKey: String
17 | }
18 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ___VARIABLE_sceneName___ {
12 |
13 | // MARK: - Entities
14 |
15 | // MARK: - ViewModels
16 |
17 | // MARK: - UseCases
18 | }
19 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ___VARIABLE_sceneName___ {
12 |
13 | // MARK: - Entities
14 |
15 | // MARK: - ViewModels
16 |
17 | // MARK: - UseCases
18 | }
19 |
--------------------------------------------------------------------------------
/Templates/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\ -\ CIS
5 |
6 | install:
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:
12 | rm -fR $(XCODE_USER_TEMPLATES_DIR)/$(TEMPLATES_DIR)
13 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/Tests/NetworkServiceTests/NetworkServiceTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import NetworkService
3 |
4 | final class NetworkServiceTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(NetworkService().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ___VARIABLE_sceneName___ {
12 |
13 | // MARK: - Entities
14 |
15 | // MARK: - ViewModels
16 |
17 | // MARK: - UseCases
18 | }
19 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ___VARIABLE_sceneName___ {
12 |
13 | // MARK: - Entities
14 |
15 | // MARK: - ViewModels
16 |
17 | // MARK: - UseCases
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # CIS
3 |
4 | [](https://swift.org)
5 | [](https://developer.apple.com/ios/)
6 |
7 | The CIS pattern is an improvement on the [Clean Swift architecture](https://clean-swift.com), aligned with advanced technologies and paradigms. It assists in the stable development of iOS and Mac applications.
8 |
9 | ## Download Templates
10 |
11 | 1. move to `Templates` Directory
12 | 2. enter command
13 | ```
14 | make
15 | ```
16 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Worker
12 |
13 | protocol ___VARIABLE_sceneName___Workable: AnyObject {
14 |
15 | }
16 |
17 | final class ___VARIABLE_sceneName___Worker: ___VARIABLE_sceneName___Workable {
18 |
19 | init() {
20 |
21 | }
22 | }
23 |
24 | // MARK: - Implement
25 |
26 | extension ___VARIABLE_sceneName___Worker {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Worker
12 |
13 | protocol ___VARIABLE_sceneName___Workable: AnyObject {
14 |
15 | }
16 |
17 | final class ___VARIABLE_sceneName___Worker: ___VARIABLE_sceneName___Workable {
18 |
19 | init() {
20 |
21 | }
22 | }
23 |
24 | // MARK: - Implement
25 |
26 | extension ___VARIABLE_sceneName___Worker {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/Sources/NetworkService/QuotationNetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuotationNetworkService.swift
3 | //
4 | //
5 | // Created by 박건우 on 2023/12/26.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol QuotationNetworkServiceProtocol {
11 | func mutateCreateQuotation(uploadUrlObjectKeys: [String], requests: String?) async throws
12 | }
13 |
14 | public final class QuotationNetworkService: QuotationNetworkServiceProtocol {
15 |
16 | public init() {}
17 |
18 | public func mutateCreateQuotation(uploadUrlObjectKeys: [String], requests: String?) async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Worker.xctemplate/Default/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Worker
12 |
13 | protocol ___VARIABLE_productName___Workable: AnyObject {
14 |
15 | }
16 |
17 | final class ___VARIABLE_productName___Worker: ___VARIABLE_productName___Workable {
18 |
19 | init() {
20 |
21 | }
22 | }
23 |
24 | // MARK: - Implement
25 |
26 | extension ___VARIABLE_productName___Worker {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/AddRequestsModels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsModels.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | public enum AddRequests {
13 |
14 | // MARK: - Entities
15 |
16 | // MARK: - ViewModels
17 |
18 | // MARK: - UseCases
19 |
20 | enum RequestQuotation {
21 |
22 | struct Request {
23 |
24 | }
25 |
26 | struct Response {
27 | let error: Error?
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Worker
12 |
13 | protocol ___VARIABLE_sceneName___Workable: AnyObject {
14 |
15 | }
16 |
17 | final class ___VARIABLE_sceneName___Worker: ___VARIABLE_sceneName___Workable {
18 |
19 | init() {
20 |
21 | }
22 | }
23 |
24 | // MARK: - Implement
25 |
26 | extension ___VARIABLE_sceneName___Worker {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Worker
12 |
13 | protocol ___VARIABLE_sceneName___Workable: AnyObject {
14 |
15 | }
16 |
17 | final class ___VARIABLE_sceneName___Worker: ___VARIABLE_sceneName___Workable {
18 |
19 | init() {
20 |
21 | }
22 | }
23 |
24 | // MARK: - Implement
25 |
26 | extension ___VARIABLE_sceneName___Worker {
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPhotoLibraryUsageDescription
6 | 명세표 첨부
7 | NSCameraUsageDescription
8 | 명세표 촬영
9 | UIApplicationSceneManifest
10 |
11 | UIApplicationSupportsMultipleScenes
12 |
13 | UISceneConfigurations
14 |
15 | UIWindowSceneSessionRoleApplication
16 |
17 |
18 | UISceneConfigurationName
19 | Default Configuration
20 | UISceneDelegateClassName
21 | ${PRODUCT_MODULE_NAME}.SceneDelegate
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "___VARIABLE_sceneName___Scene",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | .library(
11 | name: "___VARIABLE_sceneName___Scene",
12 | targets: ["___VARIABLE_sceneName___Scene"]
13 | ),
14 | ],
15 | dependencies: [
16 | ],
17 | targets: [
18 | .target(
19 | name: "___VARIABLE_sceneName___Scene",
20 | dependencies: [
21 | ]
22 | ),
23 | .testTarget(
24 | name: "___VARIABLE_sceneName___SceneTests",
25 | dependencies: [
26 | "___VARIABLE_sceneName___Scene"
27 | ]
28 | ),
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "___VARIABLE_sceneName___Scene",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | .library(
11 | name: "___VARIABLE_sceneName___Scene",
12 | targets: ["___VARIABLE_sceneName___Scene"]
13 | ),
14 | ],
15 | dependencies: [
16 | ],
17 | targets: [
18 | .target(
19 | name: "___VARIABLE_sceneName___Scene",
20 | dependencies: [
21 | ]
22 | ),
23 | .testTarget(
24 | name: "___VARIABLE_sceneName___SceneTests",
25 | dependencies: [
26 | "___VARIABLE_sceneName___Scene"
27 | ]
28 | ),
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Worker.xctemplate/Delegate/___FILEBASENAME___Worker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Delegate
12 |
13 | public protocol ___VARIABLE_productName___Delegate: AnyObject {
14 | }
15 |
16 | // MARK: - Worker
17 |
18 | protocol ___VARIABLE_productName___Workable: AnyObject {
19 | var delegate: ___VARIABLE_productName___Delegate? { get set }
20 |
21 | }
22 |
23 | final class ___VARIABLE_productName___Worker: ___VARIABLE_productName___Workable {
24 |
25 | weak var delegate: ___VARIABLE_productName___Delegate?
26 |
27 | init(delegate: ___VARIABLE_productName___Delegate) {
28 | self.delegate = delegate
29 | }
30 | }
31 |
32 | // MARK: - Implement
33 |
34 | extension ___VARIABLE_productName___Worker {
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "AddRequestsScene",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | .library(
11 | name: "AddRequestsScene",
12 | targets: ["AddRequestsScene"]
13 | ),
14 | ],
15 | dependencies: [
16 | .package(path: "../Core/NetworkService")
17 | ],
18 | targets: [
19 | .target(
20 | name: "AddRequestsScene",
21 | dependencies: [
22 | .product(name: "NetworkService", package: "NetworkService")
23 | ]
24 | ),
25 | .testTarget(
26 | name: "AddRequestsSceneTests",
27 | dependencies: [
28 | "AddRequestsScene"
29 | ]
30 | ),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Store.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyStore
7 | Description
8 | This generates a new store.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New Store Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Platforms
27 |
28 | com.apple.platform.iphoneos
29 |
30 | SortOrder
31 | 0
32 | Summary
33 | This generates a new store.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Controller.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyController
7 | Description
8 | This generates a new controller.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New Controller Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Platforms
27 |
28 | com.apple.platform.iphoneos
29 |
30 | SortOrder
31 | 0
32 | Summary
33 | This generates a new controller.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Interactor.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyInteractor
7 | Description
8 | This generates a new interactor.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New Interactor Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Platforms
27 |
28 | com.apple.platform.iphoneos
29 |
30 | SortOrder
31 | 0
32 | Summary
33 | This generates a new interactor.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/SceneTests.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MySceneTests
7 | Description
8 | This generates a new scene tests.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New SceneTests Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Platforms
27 |
28 | com.apple.platform.iphoneos
29 |
30 | SortOrder
31 | 0
32 | Summary
33 | This generates a new scene tests.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Spoqa
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.
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "UploadReceiptScene",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | .library(
11 | name: "UploadReceiptScene",
12 | targets: ["UploadReceiptScene"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/devxoul/Toaster.git", branch: "master"),
17 | .package(path: "./AddRequestsScene"),
18 | ],
19 | targets: [
20 | .target(
21 | name: "UploadReceiptScene",
22 | dependencies: [
23 | .product(name: "Toaster", package: "Toaster"),
24 | .product(name: "AddRequestsScene", package: "AddRequestsScene")
25 | ]
26 | ),
27 | .testTarget(
28 | name: "UploadReceiptSceneTests",
29 | dependencies: [
30 | "UploadReceiptScene"
31 | ]
32 | ),
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/Sources/NetworkService/ImageUploadNetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageUploadNetworkService.swift
3 | //
4 | //
5 | // Created by 박건우 on 2023/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol ImageUploadNetworkServiceProtocol {
11 | func queryImageUploadUrls(fileNames: [String]) async throws -> UploadUrlsDTO
12 | func putImageData(to url: String, imageData: Data) async throws
13 | }
14 |
15 | public final class ImageUploadNetworkService: ImageUploadNetworkServiceProtocol {
16 |
17 | public init() {}
18 |
19 | public func queryImageUploadUrls(fileNames: [String]) async throws -> UploadUrlsDTO {
20 | return UploadUrlsDTO(uploadUrls: [
21 | UploadUrlDTO(uploadUrl: "", objectKey: ""),
22 | UploadUrlDTO(uploadUrl: "", objectKey: ""),
23 | UploadUrlDTO(uploadUrl: "", objectKey: ""),
24 | UploadUrlDTO(uploadUrl: "", objectKey: ""),
25 | UploadUrlDTO(uploadUrl: "", objectKey: "")
26 | ])
27 | }
28 |
29 | public func putImageData(to url: String, imageData: Data) async throws {
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Example/Example/Service/NetworkService/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "NetworkService",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "NetworkService",
13 | targets: ["NetworkService"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "NetworkService",
24 | dependencies: []),
25 | .testTarget(
26 | name: "NetworkServiceTests",
27 | dependencies: ["NetworkService"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct ___VARIABLE_sceneName___View: View {
12 |
13 | private let controller: ___VARIABLE_sceneName___Controllerable
14 | @ObservedObject private var store: ___VARIABLE_sceneName___Store
15 |
16 | public init(
17 | initialState: ___VARIABLE_sceneName___State,
18 | controller: inout ___VARIABLE_sceneName___Controllerable?
19 | ) {
20 | let worker = ___VARIABLE_sceneName___Worker()
21 | let store = ___VARIABLE_sceneName___Store(worker: worker, state: initialState)
22 | let interactor = ___VARIABLE_sceneName___Interactor(store: store, worker: worker)
23 | let _controller = ___VARIABLE_sceneName___Controller(interactor: interactor, store: store)
24 | controller = _controller
25 |
26 | self.controller = _controller
27 | self.store = store
28 | }
29 |
30 | public var body: some View {
31 | VStack {
32 |
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/View.xctemplate/SwiftUI/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct ___VARIABLE_productName___View: View {
12 |
13 | private let controller: ___VARIABLE_productName___Controllerable
14 | @ObservedObject private var store: ___VARIABLE_productName___Store
15 |
16 | public init(
17 | initialState: ___VARIABLE_productName___State,
18 | controller: inout ___VARIABLE_productName___Controllerable?
19 | ) {
20 | let worker = ___VARIABLE_productName___Worker()
21 | let store = ___VARIABLE_productName___Store(worker: worker, state: initialState)
22 | let interactor = ___VARIABLE_productName___Interactor(store: store, worker: worker)
23 | let _controller = ___VARIABLE_productName___Controller(interactor: interactor, store: store)
24 | controller = _controller
25 |
26 | self.controller = _controller
27 | self.store = store
28 | }
29 |
30 | public var body: some View {
31 | VStack {
32 |
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct ___VARIABLE_sceneName___View: View {
12 |
13 | private let controller: ___VARIABLE_sceneName___Controllerable
14 | @ObservedObject private var store: ___VARIABLE_sceneName___Store
15 |
16 | public init(
17 | initialState: ___VARIABLE_sceneName___State,
18 | controller: inout ___VARIABLE_sceneName___Controllerable?
19 | ) {
20 | let worker = ___VARIABLE_sceneName___Worker()
21 | let store = ___VARIABLE_sceneName___Store(worker: worker, state: initialState)
22 | let interactor = ___VARIABLE_sceneName___Interactor(store: store, worker: worker)
23 | let _controller = ___VARIABLE_sceneName___Controller(interactor: interactor, store: store)
24 | controller = _controller
25 |
26 | self.controller = _controller
27 | self.store = store
28 | }
29 |
30 | public var body: some View {
31 | VStack {
32 |
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___Interactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum ___VARIABLE_sceneName___UseCase {
14 |
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol ___VARIABLE_sceneName___Interactable {
20 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___Interactable {
24 |
25 | private let store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState
26 | private let worker: ___VARIABLE_sceneName___Workable
27 |
28 | init(
29 | store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState,
30 | worker: ___VARIABLE_sceneName___Workable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension ___VARIABLE_sceneName___Interactor {
40 |
41 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
42 | switch useCase {
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___Interactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum ___VARIABLE_sceneName___UseCase {
14 |
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol ___VARIABLE_sceneName___Interactable {
20 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___Interactable {
24 |
25 | private let store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState
26 | private let worker: ___VARIABLE_sceneName___Workable
27 |
28 | init(
29 | store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState,
30 | worker: ___VARIABLE_sceneName___Workable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension ___VARIABLE_sceneName___Interactor {
40 |
41 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
42 | switch useCase {
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Interactor.xctemplate/___FILEBASENAME___Interactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum ___VARIABLE_productName___UseCase {
14 |
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol ___VARIABLE_productName___Interactable {
20 | func execute(_ useCase: ___VARIABLE_productName___UseCase) async
21 | }
22 |
23 | final class ___VARIABLE_productName___Interactor: ___VARIABLE_productName___Interactable {
24 |
25 | private let store: ___VARIABLE_productName___Mutatable & Has___VARIABLE_productName___DomainState
26 | private let worker: ___VARIABLE_productName___Workable
27 |
28 | init(
29 | store: ___VARIABLE_productName___Mutatable & Has___VARIABLE_productName___DomainState,
30 | worker: ___VARIABLE_productName___Workable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension ___VARIABLE_productName___Interactor {
40 |
41 | func execute(_ useCase: ___VARIABLE_productName___UseCase) async {
42 | switch useCase {
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Interactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum ___VARIABLE_sceneName___UseCase {
14 |
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol ___VARIABLE_sceneName___Interactable {
20 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___Interactable {
24 |
25 | private let store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState
26 | private let worker: ___VARIABLE_sceneName___Workable
27 |
28 | init(
29 | store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState,
30 | worker: ___VARIABLE_sceneName___Workable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension ___VARIABLE_sceneName___Interactor {
40 |
41 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
42 | switch useCase {
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Interactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum ___VARIABLE_sceneName___UseCase {
14 |
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol ___VARIABLE_sceneName___Interactable {
20 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Interactor: ___VARIABLE_sceneName___Interactable {
24 |
25 | private let store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState
26 | private let worker: ___VARIABLE_sceneName___Workable
27 |
28 | init(
29 | store: ___VARIABLE_sceneName___Mutatable & Has___VARIABLE_sceneName___DomainState,
30 | worker: ___VARIABLE_sceneName___Workable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension ___VARIABLE_sceneName___Interactor {
40 |
41 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
42 | switch useCase {
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Worker.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyWorker
7 | Description
8 | This generates a new worker.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New Worker Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Identifier
27 | Delegate
28 | Name
29 | include Delegate
30 | Description
31 | include Delegate
32 | Type
33 | checkbox
34 | Default
35 | false
36 |
37 |
38 | Platforms
39 |
40 | com.apple.platform.iphoneos
41 |
42 | SortOrder
43 | 0
44 | Summary
45 | This generates a new worker.
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/___FILEBASENAME___StoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_productName___Scene
11 |
12 | @MainActor class ___VARIABLE_productName___StoreTests: XCTestCase {
13 |
14 | var store: ___VARIABLE_productName___Store!
15 | @MainActor var state: ___VARIABLE_productName___State { self.store.state }
16 |
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockWorker = MockWorker()
23 | self.configure(initialState: ___VARIABLE_productName___State())
24 | }
25 |
26 | private func configure(initialState: ___VARIABLE_productName___State) {
27 | self.store = ___VARIABLE_productName___Store(worker: self.mockWorker, state: initialState)
28 | }
29 |
30 | override func tearDown() {
31 | self.store = nil
32 | self.mockWorker = nil
33 | super.tearDown()
34 | }
35 |
36 | // MARK: - Test Cases
37 |
38 | func test_mutation() {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_productName___StoreTests {
50 |
51 | class MockWorker: ___VARIABLE_productName___Workable {
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum ___VARIABLE_sceneName___Action {
14 |
15 | }
16 |
17 | // MARK: - Controller
18 |
19 | public protocol ___VARIABLE_sceneName___Controllerable: AnyObject {
20 | @discardableResult func execute(_ action: ___VARIABLE_sceneName___Action) -> Task
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Controller: ___VARIABLE_sceneName___Controllerable {
24 |
25 | private let interactor: ___VARIABLE_sceneName___Interactable
26 | private weak var store: ___VARIABLE_sceneName___Mutatable?
27 |
28 | init(
29 | interactor: ___VARIABLE_sceneName___Interactable,
30 | store: ___VARIABLE_sceneName___Mutatable
31 | ) {
32 | self.interactor = interactor
33 | self.store = store
34 | }
35 |
36 | @discardableResult public func execute(_ action: ___VARIABLE_sceneName___Action) -> Task {
37 | Task { [weak self] in
38 | await self?.execute(action)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Implement
44 |
45 | extension ___VARIABLE_sceneName___Controller {
46 |
47 | private func execute(_ action: ___VARIABLE_sceneName___Action) async {
48 | switch action {
49 |
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum ___VARIABLE_sceneName___Action {
14 |
15 | }
16 |
17 | // MARK: - Controller
18 |
19 | public protocol ___VARIABLE_sceneName___Controllerable: AnyObject {
20 | @discardableResult func execute(_ action: ___VARIABLE_sceneName___Action) -> Task
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Controller: ___VARIABLE_sceneName___Controllerable {
24 |
25 | private let interactor: ___VARIABLE_sceneName___Interactable
26 | private weak var store: ___VARIABLE_sceneName___Mutatable?
27 |
28 | init(
29 | interactor: ___VARIABLE_sceneName___Interactable,
30 | store: ___VARIABLE_sceneName___Mutatable
31 | ) {
32 | self.interactor = interactor
33 | self.store = store
34 | }
35 |
36 | @discardableResult public func execute(_ action: ___VARIABLE_sceneName___Action) -> Task {
37 | Task { [weak self] in
38 | await self?.execute(action)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Implement
44 |
45 | extension ___VARIABLE_sceneName___Controller {
46 |
47 | private func execute(_ action: ___VARIABLE_sceneName___Action) async {
48 | switch action {
49 |
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Controller.xctemplate/___FILEBASENAME___Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum ___VARIABLE_productName___Action {
14 |
15 | }
16 |
17 | // MARK: - Controller
18 |
19 | public protocol ___VARIABLE_productName___Controllerable: AnyObject {
20 | @discardableResult func execute(_ action: ___VARIABLE_productName___Action) -> Task
21 | }
22 |
23 | final class ___VARIABLE_productName___Controller: ___VARIABLE_productName___Controllerable {
24 |
25 | private let interactor: ___VARIABLE_productName___Interactable
26 | private weak var store: ___VARIABLE_productName___Mutatable?
27 |
28 | init(
29 | interactor: ___VARIABLE_productName___Interactable,
30 | store: ___VARIABLE_productName___Mutatable
31 | ) {
32 | self.interactor = interactor
33 | self.store = store
34 | }
35 |
36 | @discardableResult public func execute(_ action: ___VARIABLE_productName___Action) -> Task {
37 | Task { [weak self] in
38 | await self?.execute(action)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Implement
44 |
45 | extension ___VARIABLE_productName___Controller {
46 |
47 | private func execute(_ action: ___VARIABLE_productName___Action) async {
48 | switch action {
49 |
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/AddRequestsWorker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsWorker.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import NetworkService
10 |
11 | import Foundation
12 |
13 | // MARK: - Delegate
14 |
15 | public protocol AddRequestsDelegate: AnyObject {
16 | @MainActor func quotationRequestSuccessed(successMessage: String)
17 | }
18 |
19 | // MARK: - Worker
20 |
21 | protocol AddRequestsWorkable: AnyObject {
22 | var delegate: AddRequestsDelegate? { get set }
23 | func requestCreateQuotation(receiptImageUploadUrlObjectKeys: [String], requests: String) async throws
24 | }
25 |
26 | final class AddRequestsWorker: AddRequestsWorkable {
27 |
28 | weak var delegate: AddRequestsDelegate?
29 |
30 | private let quotationNetworkService: QuotationNetworkServiceProtocol
31 |
32 | init(
33 | delegate: AddRequestsDelegate,
34 | quotationNetworkService: QuotationNetworkServiceProtocol
35 | ) {
36 | self.delegate = delegate
37 | self.quotationNetworkService = quotationNetworkService
38 | }
39 | }
40 |
41 | // MARK: - Implement
42 |
43 | extension AddRequestsWorker {
44 |
45 | func requestCreateQuotation(receiptImageUploadUrlObjectKeys: [String], requests: String) async throws {
46 | return try await self.quotationNetworkService.mutateCreateQuotation(
47 | uploadUrlObjectKeys: receiptImageUploadUrlObjectKeys,
48 | requests: requests
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___StoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | @MainActor class ___VARIABLE_sceneName___StoreTests: XCTestCase {
13 |
14 | var store: ___VARIABLE_sceneName___Store!
15 | @MainActor var state: ___VARIABLE_sceneName___State { self.store.state }
16 |
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockWorker = MockWorker()
23 | self.configure(initialState: ___VARIABLE_sceneName___State())
24 | }
25 |
26 | private func configure(initialState: ___VARIABLE_sceneName___State) {
27 | self.store = ___VARIABLE_sceneName___Store(worker: self.mockWorker, state: initialState)
28 | }
29 |
30 | override func tearDown() {
31 | self.store = nil
32 | self.mockWorker = nil
33 | super.tearDown()
34 | }
35 |
36 | // MARK: - Test Cases
37 |
38 | func test_mutation() {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_sceneName___StoreTests {
50 |
51 | class MockWorker: ___VARIABLE_sceneName___Workable {
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___StoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | @MainActor class ___VARIABLE_sceneName___StoreTests: XCTestCase {
13 |
14 | var store: ___VARIABLE_sceneName___Store!
15 | @MainActor var state: ___VARIABLE_sceneName___State { self.store.state }
16 |
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockWorker = MockWorker()
23 | self.configure(initialState: ___VARIABLE_sceneName___State())
24 | }
25 |
26 | private func configure(initialState: ___VARIABLE_sceneName___State) {
27 | self.store = ___VARIABLE_sceneName___Store(worker: self.mockWorker, state: initialState)
28 | }
29 |
30 | override func tearDown() {
31 | self.store = nil
32 | self.mockWorker = nil
33 | super.tearDown()
34 | }
35 |
36 | // MARK: - Test Cases
37 |
38 | func test_mutation() {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_sceneName___StoreTests {
50 |
51 | class MockWorker: ___VARIABLE_sceneName___Workable {
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Example/Example/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/22.
6 | //
7 |
8 | import UploadReceiptScene
9 |
10 | import SwiftUI
11 |
12 |
13 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
14 |
15 | var window: UIWindow?
16 |
17 | weak var uploadReceiptController: UploadReceiptControllerable?
18 |
19 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
20 | if let windowScene = scene as? UIWindowScene {
21 | let window = UIWindow(windowScene: windowScene)
22 |
23 | // SwiftUI
24 | let uploadReceiptView = UploadReceiptSwiftUIView(
25 | initialState: UploadReceiptState(),
26 | controller: &self.uploadReceiptController,
27 | delegate: self
28 | )
29 | window.rootViewController = UIHostingController(rootView: uploadReceiptView)
30 |
31 | // UIKit
32 | // let uploadReceiptView = UploadReceiptUIKitView(
33 | // initialState: UploadReceiptState(),
34 | // controller: &self.uploadReceiptController,
35 | // delegate: self
36 | // )
37 | // window.rootViewController = UINavigationController(rootViewController: uploadReceiptView)
38 |
39 | self.window = window
40 | window.makeKeyAndVisible()
41 | }
42 | }
43 | }
44 |
45 | extension SceneDelegate: UploadReceiptDelegate {
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum ___VARIABLE_sceneName___Action {
14 |
15 | }
16 |
17 | // MARK: - Controller
18 |
19 | public protocol ___VARIABLE_sceneName___Controllerable: AnyObject {
20 | @discardableResult func execute(_ action: ___VARIABLE_sceneName___Action) -> Task
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Controller: ___VARIABLE_sceneName___Controllerable {
24 |
25 | private let interactor: ___VARIABLE_sceneName___Interactable
26 | private weak var store: ___VARIABLE_sceneName___Mutatable?
27 |
28 | init(
29 | interactor: ___VARIABLE_sceneName___Interactable,
30 | store: ___VARIABLE_sceneName___Mutatable
31 | ) {
32 | self.interactor = interactor
33 | self.store = store
34 | }
35 |
36 | @discardableResult public func execute(_ action: ___VARIABLE_sceneName___Action) -> Task {
37 | Task { [weak self] in
38 | await self?.execute(action)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Implement
44 |
45 | extension ___VARIABLE_sceneName___Controller {
46 |
47 | private func execute(_ action: ___VARIABLE_sceneName___Action) async {
48 | switch action {
49 |
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum ___VARIABLE_sceneName___Action {
14 |
15 | }
16 |
17 | // MARK: - Controller
18 |
19 | public protocol ___VARIABLE_sceneName___Controllerable: AnyObject {
20 | @discardableResult func execute(_ action: ___VARIABLE_sceneName___Action) -> Task
21 | }
22 |
23 | final class ___VARIABLE_sceneName___Controller: ___VARIABLE_sceneName___Controllerable {
24 |
25 | private let interactor: ___VARIABLE_sceneName___Interactable
26 | private weak var store: ___VARIABLE_sceneName___Mutatable?
27 |
28 | init(
29 | interactor: ___VARIABLE_sceneName___Interactable,
30 | store: ___VARIABLE_sceneName___Mutatable
31 | ) {
32 | self.interactor = interactor
33 | self.store = store
34 | }
35 |
36 | @discardableResult public func execute(_ action: ___VARIABLE_sceneName___Action) -> Task {
37 | Task { [weak self] in
38 | await self?.execute(action)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Implement
44 |
45 | extension ___VARIABLE_sceneName___Controller {
46 |
47 | private func execute(_ action: ___VARIABLE_sceneName___Action) async {
48 | switch action {
49 |
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/View.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyView
7 | Description
8 | This generates a new view.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New View Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Identifier
27 | uiFramework
28 | Required
29 |
30 | Name
31 | UI Framework:
32 | Description
33 | The type of classes to create
34 | Type
35 | popup
36 | Default
37 | SwiftUI
38 | Values
39 |
40 | SwiftUI
41 | UIKit
42 |
43 |
44 |
45 | Platforms
46 |
47 | com.apple.platform.iphoneos
48 |
49 | SortOrder
50 | 0
51 | Summary
52 | This generates a new scene using Uncle Bob's clean architecture.
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/AddRequestsController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsController.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Action
12 |
13 | public enum AddRequestsAction {
14 | case newRequestsTextChanged(newValue: String)
15 | case quotationRequestButtonTapped
16 | }
17 |
18 | // MARK: - Controller
19 |
20 | public protocol AddRequestsControllerable: AnyObject {
21 | @discardableResult func execute(_ action: AddRequestsAction) -> Task
22 | }
23 |
24 | final class AddRequestsController: AddRequestsControllerable {
25 |
26 | private let interactor: AddRequestsInteractable
27 | private weak var store: AddRequestsMutatable?
28 |
29 | init(
30 | interactor: AddRequestsInteractable,
31 | store: AddRequestsMutatable
32 | ) {
33 | self.interactor = interactor
34 | self.store = store
35 | }
36 |
37 | @discardableResult public func execute(_ action: AddRequestsAction) -> Task {
38 | Task { [weak self] in
39 | await self?.execute(action)
40 | }
41 | }
42 | }
43 |
44 | // MARK: - Implement
45 |
46 | extension AddRequestsController {
47 |
48 | private func execute(_ action: AddRequestsAction) async {
49 | switch action {
50 | case .newRequestsTextChanged(let newValue):
51 | await self.store?.execute(.setRequests(requests: newValue))
52 |
53 | case .quotationRequestButtonTapped:
54 | await self.interactor.execute(.requestQuotation(request: AddRequests.RequestQuotation.Request()))
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import UIKit
11 |
12 | public final class ___VARIABLE_sceneName___View: UIViewController {
13 | private var cancellables: Set = []
14 |
15 | private let controller: ___VARIABLE_sceneName___Controllerable
16 | private var store: ___VARIABLE_sceneName___Store
17 |
18 | public init(
19 | initialState: ___VARIABLE_sceneName___State,
20 | controller: inout ___VARIABLE_sceneName___Controllerable?
21 | ) {
22 | let worker = ___VARIABLE_sceneName___Worker()
23 | let store = ___VARIABLE_sceneName___Store(worker: worker, state: initialState)
24 | let interactor = ___VARIABLE_sceneName___Interactor(store: store, worker: worker)
25 | let _controller = ___VARIABLE_sceneName___Controller(interactor: interactor, store: store)
26 | controller = _controller
27 |
28 | self.controller = _controller
29 | self.store = store
30 |
31 | super.init(nibName: nil, bundle: nil)
32 |
33 | self.bind()
34 | }
35 |
36 | required init?(coder aDecoder: NSCoder) {
37 | fatalError("init(coder:) has not been implemented")
38 | }
39 |
40 | // MARK: - UI
41 |
42 | // MARK: - View lifecycle
43 |
44 | public override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | self.setupUI()
48 | }
49 |
50 | // MARK: - Layout
51 |
52 | private func setupUI() {
53 |
54 | }
55 |
56 | // MARK: - Bind
57 |
58 | private func bind() {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/___FILEBASENAME___InteractorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_productName___Scene
11 |
12 | class ___VARIABLE_productName___InteractorTests: XCTestCase {
13 |
14 | var interactor: ___VARIABLE_productName___Interactor!
15 |
16 | var mockStore: MockStore!
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockStore = MockStore()
23 | self.mockWorker = MockWorker()
24 | self.interactor = ___VARIABLE_productName___Interactor(store: self.mockStore, worker: self.mockWorker)
25 | }
26 |
27 | override func tearDown() {
28 | self.interactor = nil
29 | self.mockStore = nil
30 | self.mockWorker = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | // MARK: - Test UseCase
37 |
38 | func test_usecase() async {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_productName___InteractorTests {
50 |
51 | class MockStore: ___VARIABLE_productName___Mutatable, Has___VARIABLE_productName___DomainState {
52 | @MainActor var domainState = ___VARIABLE_productName___State.DomainState()
53 |
54 | @MainActor func execute(_ mutation: ___VARIABLE_productName___Mutation) {
55 | switch mutation {
56 | default:
57 | break
58 | }
59 | }
60 | }
61 |
62 | class MockWorker: ___VARIABLE_productName___Workable {
63 |
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/View.xctemplate/UIKit/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import UIKit
11 |
12 | public final class ___VARIABLE_productName___View: UIViewController {
13 | private var cancellables: Set = []
14 |
15 | private let controller: ___VARIABLE_productName___Controllerable
16 | private var store: ___VARIABLE_productName___Store
17 |
18 | public init(
19 | initialState: ___VARIABLE_productName___State,
20 | controller: inout ___VARIABLE_productName___Controllerable?
21 | ) {
22 | let worker = ___VARIABLE_productName___Worker()
23 | let store = ___VARIABLE_productName___Store(worker: worker, state: initialState)
24 | let interactor = ___VARIABLE_productName___Interactor(store: store, worker: worker)
25 | let _controller = ___VARIABLE_productName___Controller(interactor: interactor, store: store)
26 | controller = _controller
27 |
28 | self.controller = _controller
29 | self.store = store
30 |
31 | super.init(nibName: nil, bundle: nil)
32 |
33 | self.bind()
34 | }
35 |
36 | required init?(coder aDecoder: NSCoder) {
37 | fatalError("init(coder:) has not been implemented")
38 | }
39 |
40 | // MARK: - UI
41 |
42 | // MARK: - View lifecycle
43 |
44 | public override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | self.setupUI()
48 | }
49 |
50 | // MARK: - Layout
51 |
52 | private func setupUI() {
53 |
54 | }
55 |
56 | // MARK: - Bind
57 |
58 | private func bind() {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/___FILEBASENAME___ControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_productName___Scene
11 |
12 | class ___VARIABLE_productName___ControllerTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_productName___Controller!
15 |
16 | var mockInteractor: MockInteractor!
17 | var mockStore: MockStore!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockInteractor = MockInteractor()
23 | self.mockStore = MockStore()
24 | self.controller = ___VARIABLE_productName___Controller(interactor: self.mockInteractor, store: self.mockStore)
25 | }
26 |
27 | override func tearDown() {
28 | self.controller = nil
29 | self.mockInteractor = nil
30 | self.mockStore = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | func test_action() async {
37 | // Given
38 |
39 | // When
40 |
41 | // Then
42 | }
43 | }
44 |
45 | // MARK: - Mock Classes
46 |
47 | extension ___VARIABLE_productName___ControllerTests {
48 |
49 | class MockInteractor: ___VARIABLE_productName___Interactable {
50 |
51 | func execute(_ useCase: ___VARIABLE_productName___UseCase) async {
52 | switch useCase {
53 |
54 | }
55 | }
56 | }
57 |
58 | class MockStore: ___VARIABLE_productName___Mutatable {
59 |
60 | @MainActor func execute(_ mutation: ___VARIABLE_productName___Mutation) {
61 | switch mutation {
62 | default:
63 | break
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import UIKit
11 |
12 | public final class ___VARIABLE_sceneName___View: UIViewController {
13 | private var cancellables: Set = []
14 |
15 | private let controller: ___VARIABLE_sceneName___Controllerable
16 | private var store: ___VARIABLE_sceneName___Store
17 |
18 | public init(
19 | initialState: ___VARIABLE_sceneName___State,
20 | controller: inout ___VARIABLE_sceneName___Controllerable?
21 | ) {
22 | let worker = ___VARIABLE_sceneName___Worker()
23 | let store = ___VARIABLE_sceneName___Store(worker: worker, state: initialState)
24 | let interactor = ___VARIABLE_sceneName___Interactor(store: store, worker: worker)
25 | let _controller = ___VARIABLE_sceneName___Controller(interactor: interactor, store: store)
26 | controller = _controller
27 |
28 | self.controller = _controller
29 | self.store = store
30 |
31 | super.init(nibName: nil, bundle: nil)
32 |
33 | self.bind()
34 | }
35 |
36 | required init?(coder aDecoder: NSCoder) {
37 | fatalError("init(coder:) has not been implemented")
38 | }
39 |
40 | // MARK: - UI
41 |
42 | // MARK: - View lifecycle
43 |
44 | public override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | self.setupUI()
48 | }
49 |
50 | // MARK: - Layout
51 |
52 | private func setupUI() {
53 |
54 | }
55 |
56 | // MARK: - Bind
57 |
58 | private func bind() {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___InteractorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___InteractorTests: XCTestCase {
13 |
14 | var interactor: ___VARIABLE_sceneName___Interactor!
15 |
16 | var mockStore: MockStore!
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockStore = MockStore()
23 | self.mockWorker = MockWorker()
24 | self.interactor = ___VARIABLE_sceneName___Interactor(store: self.mockStore, worker: self.mockWorker)
25 | }
26 |
27 | override func tearDown() {
28 | self.interactor = nil
29 | self.mockStore = nil
30 | self.mockWorker = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | // MARK: - Test UseCase
37 |
38 | func test_usecase() async {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_sceneName___InteractorTests {
50 |
51 | class MockStore: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState {
52 | @MainActor var domainState = ___VARIABLE_sceneName___State.DomainState()
53 |
54 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | switch mutation {
56 | default:
57 | break
58 | }
59 | }
60 | }
61 |
62 | class MockWorker: ___VARIABLE_sceneName___Workable {
63 |
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___InteractorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___InteractorTests: XCTestCase {
13 |
14 | var interactor: ___VARIABLE_sceneName___Interactor!
15 |
16 | var mockStore: MockStore!
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockStore = MockStore()
23 | self.mockWorker = MockWorker()
24 | self.interactor = ___VARIABLE_sceneName___Interactor(store: self.mockStore, worker: self.mockWorker)
25 | }
26 |
27 | override func tearDown() {
28 | self.interactor = nil
29 | self.mockStore = nil
30 | self.mockWorker = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | // MARK: - Test UseCase
37 |
38 | func test_usecase() async {
39 | // Given
40 |
41 | // When
42 |
43 | // Then
44 | }
45 | }
46 |
47 | // MARK: - Mock Classes
48 |
49 | extension ___VARIABLE_sceneName___InteractorTests {
50 |
51 | class MockStore: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState {
52 | @MainActor var domainState = ___VARIABLE_sceneName___State.DomainState()
53 |
54 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | switch mutation {
56 | default:
57 | break
58 | }
59 | }
60 | }
61 |
62 | class MockWorker: ___VARIABLE_sceneName___Workable {
63 |
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___ControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___ControllerTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_sceneName___Controller!
15 |
16 | var mockInteractor: MockInteractor!
17 | var mockStore: MockStore!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockInteractor = MockInteractor()
23 | self.mockStore = MockStore()
24 | self.controller = ___VARIABLE_sceneName___Controller(interactor: self.mockInteractor, store: self.mockStore)
25 | }
26 |
27 | override func tearDown() {
28 | self.controller = nil
29 | self.mockInteractor = nil
30 | self.mockStore = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | func test_action() async {
37 | // Given
38 |
39 | // When
40 |
41 | // Then
42 | }
43 | }
44 |
45 | // MARK: - Mock Classes
46 |
47 | extension ___VARIABLE_sceneName___ControllerTests {
48 |
49 | class MockInteractor: ___VARIABLE_sceneName___Interactable {
50 |
51 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
52 | switch useCase {
53 |
54 | }
55 | }
56 | }
57 |
58 | class MockStore: ___VARIABLE_sceneName___Mutatable {
59 |
60 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
61 | switch mutation {
62 | default:
63 | break
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/Unit/___FILEBASENAME___ControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___ControllerTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_sceneName___Controller!
15 |
16 | var mockInteractor: MockInteractor!
17 | var mockStore: MockStore!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockInteractor = MockInteractor()
23 | self.mockStore = MockStore()
24 | self.controller = ___VARIABLE_sceneName___Controller(interactor: self.mockInteractor, store: self.mockStore)
25 | }
26 |
27 | override func tearDown() {
28 | self.controller = nil
29 | self.mockInteractor = nil
30 | self.mockStore = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | func test_action() async {
37 | // Given
38 |
39 | // When
40 |
41 | // Then
42 | }
43 | }
44 |
45 | // MARK: - Mock Classes
46 |
47 | extension ___VARIABLE_sceneName___ControllerTests {
48 |
49 | class MockInteractor: ___VARIABLE_sceneName___Interactable {
50 |
51 | func execute(_ useCase: ___VARIABLE_sceneName___UseCase) async {
52 | switch useCase {
53 |
54 | }
55 | }
56 | }
57 |
58 | class MockStore: ___VARIABLE_sceneName___Mutatable {
59 |
60 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
61 | switch mutation {
62 | default:
63 | break
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/SceneTests.xctemplate/___FILEBASENAME___SceneTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_productName___Scene
11 |
12 | class ___VARIABLE_productName___SceneTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_productName___Controller!
15 | @MainActor var state: ___VARIABLE_productName___State { self.store.state }
16 |
17 | var store: ___VARIABLE_productName___Store!
18 | var interactor: ___VARIABLE_productName___Interactor!
19 |
20 | var mockWorker: MockWorker!
21 |
22 | @MainActor override func setUp() {
23 | super.setUp()
24 |
25 | self.mockWorker = MockWorker()
26 | self.configure(initialState: ___VARIABLE_productName___State())
27 | }
28 |
29 | @MainActor private func configure(initialState: ___VARIABLE_productName___State) {
30 | self.store = ___VARIABLE_productName___Store(worker: self.mockWorker, state: initialState)
31 | self.interactor = ___VARIABLE_productName___Interactor(store: self.store, worker: self.mockWorker)
32 | self.controller = ___VARIABLE_productName___Controller(interactor: self.interactor, store: self.store)
33 | }
34 |
35 | override func tearDown() {
36 | self.controller = nil
37 | self.store = nil
38 | self.interactor = nil
39 | self.mockWorker = nil
40 | super.tearDown()
41 | }
42 |
43 | // MARK: - Test Scenarios
44 |
45 | func test_scenario() async {
46 | // Given
47 |
48 | // When
49 |
50 | // Then
51 | }
52 | }
53 |
54 | // MARK: - Mock Classes
55 |
56 | extension ___VARIABLE_productName___SceneTests {
57 |
58 | class MockWorker: ___VARIABLE_productName___Workable {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/___FILEBASENAME___SceneTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___SceneTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_sceneName___Controller!
15 | @MainActor var state: ___VARIABLE_sceneName___State { self.store.state }
16 |
17 | var store: ___VARIABLE_sceneName___Store!
18 | var interactor: ___VARIABLE_sceneName___Interactor!
19 |
20 | var mockWorker: MockWorker!
21 |
22 | @MainActor override func setUp() {
23 | super.setUp()
24 |
25 | self.mockWorker = MockWorker()
26 | self.configure(initialState: ___VARIABLE_sceneName___State())
27 | }
28 |
29 | @MainActor private func configure(initialState: ___VARIABLE_sceneName___State) {
30 | self.store = ___VARIABLE_sceneName___Store(worker: self.mockWorker, state: initialState)
31 | self.interactor = ___VARIABLE_sceneName___Interactor(store: self.store, worker: self.mockWorker)
32 | self.controller = ___VARIABLE_sceneName___Controller(interactor: self.interactor, store: self.store)
33 | }
34 |
35 | override func tearDown() {
36 | self.controller = nil
37 | self.store = nil
38 | self.interactor = nil
39 | self.mockWorker = nil
40 | super.tearDown()
41 | }
42 |
43 | // MARK: - Test Scenarios
44 |
45 | func test_scenario() async {
46 | // Given
47 |
48 | // When
49 |
50 | // Then
51 | }
52 | }
53 |
54 | // MARK: - Mock Classes
55 |
56 | extension ___VARIABLE_sceneName___SceneTests {
57 |
58 | class MockWorker: ___VARIABLE_sceneName___Workable {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Tests/___FILEBASENAME___SceneTests/___FILEBASENAME___SceneTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ___VARIABLE_sceneName___Scene
11 |
12 | class ___VARIABLE_sceneName___SceneTests: XCTestCase {
13 |
14 | var controller: ___VARIABLE_sceneName___Controller!
15 | @MainActor var state: ___VARIABLE_sceneName___State { self.store.state }
16 |
17 | var store: ___VARIABLE_sceneName___Store!
18 | var interactor: ___VARIABLE_sceneName___Interactor!
19 |
20 | var mockWorker: MockWorker!
21 |
22 | @MainActor override func setUp() {
23 | super.setUp()
24 |
25 | self.mockWorker = MockWorker()
26 | self.configure(initialState: ___VARIABLE_sceneName___State())
27 | }
28 |
29 | @MainActor private func configure(initialState: ___VARIABLE_sceneName___State) {
30 | self.store = ___VARIABLE_sceneName___Store(worker: self.mockWorker, state: initialState)
31 | self.interactor = ___VARIABLE_sceneName___Interactor(store: self.store, worker: self.mockWorker)
32 | self.controller = ___VARIABLE_sceneName___Controller(interactor: self.interactor, store: self.store)
33 | }
34 |
35 | override func tearDown() {
36 | self.controller = nil
37 | self.store = nil
38 | self.interactor = nil
39 | self.mockWorker = nil
40 | super.tearDown()
41 | }
42 |
43 | // MARK: - Test Scenarios
44 |
45 | func test_scenario() async {
46 | // Given
47 |
48 | // When
49 |
50 | // Then
51 | }
52 | }
53 |
54 | // MARK: - Mock Classes
55 |
56 | extension ___VARIABLE_sceneName___SceneTests {
57 |
58 | class MockWorker: ___VARIABLE_sceneName___Workable {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/AddRequestsInteractor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsInteractor.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum AddRequestsUseCase {
14 | case requestQuotation(request: AddRequests.RequestQuotation.Request)
15 | }
16 |
17 | // MARK: - Interactor
18 |
19 | protocol AddRequestsInteractable {
20 | func execute(_ useCase: AddRequestsUseCase) async
21 | }
22 |
23 | final class AddRequestsInteractor: AddRequestsInteractable {
24 |
25 | private let store: AddRequestsMutatable & HasAddRequestsDomainState
26 | private let worker: AddRequestsWorkable
27 |
28 | init(
29 | store: AddRequestsMutatable & HasAddRequestsDomainState,
30 | worker: AddRequestsWorkable
31 | ) {
32 | self.store = store
33 | self.worker = worker
34 | }
35 | }
36 |
37 | // MARK: - Implement
38 |
39 | extension AddRequestsInteractor {
40 |
41 | func execute(_ useCase: AddRequestsUseCase) async {
42 | switch useCase {
43 | case .requestQuotation(_):
44 | let response: AddRequests.RequestQuotation.Response
45 | let storedReceiptImageUploadUrlObjectKeys = await self.store.domainState.receiptImageUploadUrlObjectKeys
46 | let storedRequests = await self.store.domainState.requests
47 |
48 | do {
49 | try await self.worker.requestCreateQuotation(
50 | receiptImageUploadUrlObjectKeys: storedReceiptImageUploadUrlObjectKeys,
51 | requests: storedRequests
52 | )
53 | response = AddRequests.RequestQuotation.Response(error: nil)
54 | }
55 | catch {
56 | response = AddRequests.RequestQuotation.Response(error: error)
57 | }
58 |
59 | await self.store.execute(.mutateRequestQuotation(response: response))
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/SwiftUI/___FILEBASENAME___Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum ___VARIABLE_sceneName___Mutation {
14 |
15 | }
16 |
17 | // MARK: - State
18 |
19 | @MainActor public struct ___VARIABLE_sceneName___State {
20 |
21 | var domainState = DomainState()
22 |
23 | struct DomainState {
24 |
25 | }
26 |
27 | public init() {}
28 | }
29 |
30 | // MARK: - Store
31 |
32 | protocol ___VARIABLE_sceneName___Mutatable: AnyObject {
33 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation)
34 | }
35 |
36 | protocol Has___VARIABLE_sceneName___DomainState: AnyObject {
37 | @MainActor var domainState: ___VARIABLE_sceneName___State.DomainState { get }
38 | }
39 |
40 | @MainActor final class ___VARIABLE_sceneName___Store: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState, ObservableObject {
41 |
42 | let worker: ___VARIABLE_sceneName___Workable
43 | @Published private(set) var state: ___VARIABLE_sceneName___State
44 | var domainState: ___VARIABLE_sceneName___State.DomainState { self.state.domainState }
45 |
46 | init(
47 | worker: ___VARIABLE_sceneName___Workable,
48 | state: ___VARIABLE_sceneName___State
49 | ) {
50 | self.worker = worker
51 | self.state = state
52 | }
53 |
54 | func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | self.state = self.execute(state: self.state, mutation: mutation)
56 | }
57 | }
58 |
59 | // MARK: - Implement
60 |
61 | extension ___VARIABLE_sceneName___Store {
62 |
63 | private func execute(state: ___VARIABLE_sceneName___State, mutation: ___VARIABLE_sceneName___Mutation) -> ___VARIABLE_sceneName___State {
64 | var state = state
65 |
66 | switch mutation {
67 |
68 | }
69 |
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene.xctemplate/UIKit/___FILEBASENAME___Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum ___VARIABLE_sceneName___Mutation {
14 |
15 | }
16 |
17 | // MARK: - State
18 |
19 | @MainActor public struct ___VARIABLE_sceneName___State {
20 |
21 | var domainState = DomainState()
22 |
23 | struct DomainState {
24 |
25 | }
26 |
27 | public init() {}
28 | }
29 |
30 | // MARK: - Store
31 |
32 | protocol ___VARIABLE_sceneName___Mutatable: AnyObject {
33 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation)
34 | }
35 |
36 | protocol Has___VARIABLE_sceneName___DomainState: AnyObject {
37 | @MainActor var domainState: ___VARIABLE_sceneName___State.DomainState { get }
38 | }
39 |
40 | @MainActor final class ___VARIABLE_sceneName___Store: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState, ObservableObject {
41 |
42 | let worker: ___VARIABLE_sceneName___Workable
43 | @Published private(set) var state: ___VARIABLE_sceneName___State
44 | var domainState: ___VARIABLE_sceneName___State.DomainState { self.state.domainState }
45 |
46 | init(
47 | worker: ___VARIABLE_sceneName___Workable,
48 | state: ___VARIABLE_sceneName___State
49 | ) {
50 | self.worker = worker
51 | self.state = state
52 | }
53 |
54 | func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | self.state = self.execute(state: self.state, mutation: mutation)
56 | }
57 | }
58 |
59 | // MARK: - Implement
60 |
61 | extension ___VARIABLE_sceneName___Store {
62 |
63 | private func execute(state: ___VARIABLE_sceneName___State, mutation: ___VARIABLE_sceneName___Mutation) -> ___VARIABLE_sceneName___State {
64 | var state = state
65 |
66 | switch mutation {
67 |
68 | }
69 |
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Store.xctemplate/___FILEBASENAME___Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum ___VARIABLE_productName___Mutation {
14 |
15 | }
16 |
17 | // MARK: - State
18 |
19 | @MainActor public struct ___VARIABLE_productName___State {
20 |
21 | var domainState = DomainState()
22 |
23 | struct DomainState {
24 |
25 | }
26 |
27 | public init() {}
28 | }
29 |
30 | // MARK: - Store
31 |
32 | protocol ___VARIABLE_productName___Mutatable: AnyObject {
33 | @MainActor func execute(_ mutation: ___VARIABLE_productName___Mutation)
34 | }
35 |
36 | protocol Has___VARIABLE_productName___DomainState: AnyObject {
37 | @MainActor var domainState: ___VARIABLE_productName___State.DomainState { get }
38 | }
39 |
40 | @MainActor final class ___VARIABLE_productName___Store: ___VARIABLE_productName___Mutatable, Has___VARIABLE_productName___DomainState, ObservableObject {
41 |
42 | let worker: ___VARIABLE_productName___Workable
43 | @Published private(set) var state: ___VARIABLE_productName___State
44 | var domainState: ___VARIABLE_productName___State.DomainState { self.state.domainState }
45 |
46 | init(
47 | worker: ___VARIABLE_productName___Workable,
48 | state: ___VARIABLE_productName___State
49 | ) {
50 | self.worker = worker
51 | self.state = state
52 | }
53 |
54 | func execute(_ mutation: ___VARIABLE_productName___Mutation) {
55 | self.state = self.execute(state: self.state, mutation: mutation)
56 | }
57 | }
58 |
59 | // MARK: - Implement
60 |
61 | extension ___VARIABLE_productName___Store {
62 |
63 | private func execute(state: ___VARIABLE_productName___State, mutation: ___VARIABLE_productName___Mutation) -> ___VARIABLE_productName___State {
64 | var state = state
65 |
66 | switch mutation {
67 |
68 | }
69 |
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/UIKit/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum ___VARIABLE_sceneName___Mutation {
14 |
15 | }
16 |
17 | // MARK: - State
18 |
19 | @MainActor public struct ___VARIABLE_sceneName___State {
20 |
21 | var domainState = DomainState()
22 |
23 | struct DomainState {
24 |
25 | }
26 |
27 | public init() {}
28 | }
29 |
30 | // MARK: - Store
31 |
32 | protocol ___VARIABLE_sceneName___Mutatable: AnyObject {
33 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation)
34 | }
35 |
36 | protocol Has___VARIABLE_sceneName___DomainState: AnyObject {
37 | @MainActor var domainState: ___VARIABLE_sceneName___State.DomainState { get }
38 | }
39 |
40 | @MainActor final class ___VARIABLE_sceneName___Store: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState, ObservableObject {
41 |
42 | let worker: ___VARIABLE_sceneName___Workable
43 | @Published private(set) var state: ___VARIABLE_sceneName___State
44 | var domainState: ___VARIABLE_sceneName___State.DomainState { self.state.domainState }
45 |
46 | init(
47 | worker: ___VARIABLE_sceneName___Workable,
48 | state: ___VARIABLE_sceneName___State
49 | ) {
50 | self.worker = worker
51 | self.state = state
52 | }
53 |
54 | func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | self.state = self.execute(state: self.state, mutation: mutation)
56 | }
57 | }
58 |
59 | // MARK: - Implement
60 |
61 | extension ___VARIABLE_sceneName___Store {
62 |
63 | private func execute(state: ___VARIABLE_sceneName___State, mutation: ___VARIABLE_sceneName___Mutation) -> ___VARIABLE_sceneName___State {
64 | var state = state
65 |
66 | switch mutation {
67 |
68 | }
69 |
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.xctemplate/SwiftUI/___FILEBASENAME___Scene/Sources/___FILEBASENAME___Scene/___FILEBASENAME___Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ___FILENAME___
3 | // ___PROJECTNAME___
4 | //
5 | // Created by ___FULLUSERNAME___ on ___DATE___.
6 | // Copyright (c) ___YEAR___ Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum ___VARIABLE_sceneName___Mutation {
14 |
15 | }
16 |
17 | // MARK: - State
18 |
19 | @MainActor public struct ___VARIABLE_sceneName___State {
20 |
21 | var domainState = DomainState()
22 |
23 | struct DomainState {
24 |
25 | }
26 |
27 | public init() {}
28 | }
29 |
30 | // MARK: - Store
31 |
32 | protocol ___VARIABLE_sceneName___Mutatable: AnyObject {
33 | @MainActor func execute(_ mutation: ___VARIABLE_sceneName___Mutation)
34 | }
35 |
36 | protocol Has___VARIABLE_sceneName___DomainState: AnyObject {
37 | @MainActor var domainState: ___VARIABLE_sceneName___State.DomainState { get }
38 | }
39 |
40 | @MainActor final class ___VARIABLE_sceneName___Store: ___VARIABLE_sceneName___Mutatable, Has___VARIABLE_sceneName___DomainState, ObservableObject {
41 |
42 | let worker: ___VARIABLE_sceneName___Workable
43 | @Published private(set) var state: ___VARIABLE_sceneName___State
44 | var domainState: ___VARIABLE_sceneName___State.DomainState { self.state.domainState }
45 |
46 | init(
47 | worker: ___VARIABLE_sceneName___Workable,
48 | state: ___VARIABLE_sceneName___State
49 | ) {
50 | self.worker = worker
51 | self.state = state
52 | }
53 |
54 | func execute(_ mutation: ___VARIABLE_sceneName___Mutation) {
55 | self.state = self.execute(state: self.state, mutation: mutation)
56 | }
57 | }
58 |
59 | // MARK: - Implement
60 |
61 | extension ___VARIABLE_sceneName___Store {
62 |
63 | private func execute(state: ___VARIABLE_sceneName___State, mutation: ___VARIABLE_sceneName___Mutation) -> ___VARIABLE_sceneName___State {
64 | var state = state
65 |
66 | switch mutation {
67 |
68 | }
69 |
70 | return state
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/View/Any/AddRequestsSwiftUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsSwiftUIView.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import NetworkService
10 |
11 | import SwiftUI
12 |
13 | public struct AddRequestsSwiftUIView: View {
14 |
15 | private var controller: AddRequestsControllerable
16 | @ObservedObject private var store: AddRequestsStore
17 |
18 | public init(
19 | initialState: AddRequestsState,
20 | controller: inout AddRequestsControllerable?,
21 | delegate: AddRequestsDelegate
22 | ) {
23 | let quotationNetworkService = QuotationNetworkService()
24 | let worker = AddRequestsWorker(delegate: delegate, quotationNetworkService: quotationNetworkService)
25 | let store = AddRequestsStore(worker: worker, state: initialState)
26 | let interactor = AddRequestsInteractor(store: store, worker: worker)
27 | let _controller = AddRequestsController(interactor: interactor, store: store)
28 | controller = _controller
29 |
30 | self.controller = _controller
31 | self.store = store
32 | }
33 |
34 | public var body: some View {
35 | VStack {
36 | TextField("요청 사항 입력", text: Binding(
37 | get: {
38 | return self.store.state.requestsText
39 | },
40 | set: { value in
41 | self.controller.execute(.newRequestsTextChanged(newValue: value))
42 | }
43 | ))
44 | .textFieldStyle(RoundedBorderTextFieldStyle())
45 | .padding(.top, 20)
46 | .padding(.horizontal, 10)
47 |
48 | Text("견적 요청")
49 | .foregroundColor(.white)
50 | .frame(maxWidth: .infinity, minHeight: 50)
51 | .background(Color.blue)
52 | .cornerRadius(10)
53 | .padding(.horizontal, 10)
54 | .onTapGesture {
55 | self.controller.execute(.quotationRequestButtonTapped)
56 | }
57 |
58 | Text(self.store.state.message)
59 |
60 | Spacer()
61 | }
62 | .navigationBarTitle("견적 요청 - 요청 사항", displayMode: .inline)
63 | .navigationBarBackButtonHidden(false)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/UploadReceiptModels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptModels.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | public enum UploadReceipt {
13 |
14 | // MARK: - Entities
15 |
16 | public struct ImageModel: Equatable {
17 | let name: String = "\(Date().timeIntervalSince1970)_\(UUID())"
18 | let data: Data
19 | let uiData: ImageUIDataModel
20 | }
21 |
22 | public struct ImageUploadUrl {
23 | let uploadUrl: String
24 | let objectKey: String
25 | }
26 |
27 | // MARK: - ViewModels
28 |
29 | public typealias ImageUIDataModel = UIImage
30 |
31 | // MARK: - UseCases
32 |
33 | enum AttachImage {
34 |
35 | struct Request {
36 | let imageUIData: ImageUIDataModel
37 | }
38 |
39 | struct Response {
40 | let attachedImages: [ImageModel]
41 | let error: Error?
42 |
43 | enum Error {
44 | case exceedImageCount
45 | case failDataMapping
46 | }
47 | }
48 | }
49 |
50 | enum UploadImage {
51 |
52 | struct Request {
53 |
54 | }
55 |
56 | struct Response {
57 | let uploadUrlObjectKeys: [String]
58 | let error: Error?
59 |
60 | enum Error {
61 | case emptyAttachedImage
62 | case `default`(Swift.Error)
63 | }
64 | }
65 | }
66 |
67 | enum SaveImage {
68 |
69 | struct Request {
70 | let imageUIData: ImageUIDataModel
71 | }
72 |
73 | struct Response {
74 | let error: Error?
75 | }
76 | }
77 |
78 | enum ShowCamera {
79 |
80 | struct Request {
81 |
82 | }
83 |
84 | struct Response {
85 | let permissionDenied: Bool
86 | }
87 | }
88 |
89 | enum ShowGallery {
90 |
91 | struct Request {
92 |
93 | }
94 |
95 | struct Response {
96 | let permissionDenied: Bool
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/UnitTests.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DefaultCompletionName
6 | MyUnitTests
7 | Description
8 | This generates new controller tests, interactor tests, store tests.
9 | Kind
10 | Xcode.IDEKit.TextSubstitutionFileTemplateKind
11 | Options
12 |
13 |
14 | Type
15 | text
16 | NotPersisted
17 |
18 | Name
19 | New UnitTests Name:
20 | Default
21 | New
22 | Identifier
23 | productName
24 |
25 |
26 | Default
27 | ___VARIABLE_productName:identifier___ControllerTests
28 | Description
29 | The controllerTests name
30 | Identifier
31 | controllerTestsName
32 | Name
33 | ControllerTests Name:
34 | Required
35 |
36 | Type
37 | static
38 |
39 |
40 | Default
41 | ___VARIABLE_productName:identifier___InteractorTests
42 | Description
43 | The interactorTests name
44 | Identifier
45 | interactorTestsName
46 | Name
47 | interactorTests Name:
48 | Required
49 |
50 | Type
51 | static
52 |
53 |
54 | Default
55 | ___VARIABLE_productName:identifier___StoreTests
56 | Description
57 | The storeTests name
58 | Identifier
59 | storeTestsName
60 | Name
61 | StoreTests Name:
62 | Required
63 |
64 | Type
65 | static
66 |
67 |
68 | Platforms
69 |
70 | com.apple.platform.iphoneos
71 |
72 | SortOrder
73 | 0
74 | Summary
75 | This generates new controller tests, interactor tests, store tests.
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Tests/AddRequestsSceneTests/Unit/AddRequestsControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsControllerTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/23.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import AddRequestsScene
11 |
12 | class AddRequestsControllerTests: XCTestCase {
13 |
14 | var controller: AddRequestsController!
15 |
16 | var mockInteractor: MockInteractor!
17 | var mockStore: MockStore!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockInteractor = MockInteractor()
23 | self.mockStore = MockStore()
24 | self.controller = AddRequestsController(interactor: self.mockInteractor, store: self.mockStore)
25 | }
26 |
27 | override func tearDown() {
28 | self.controller = nil
29 | self.mockInteractor = nil
30 | self.mockStore = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | func test_newRequestsTextChanged() async {
37 | // Given
38 | let dummyNewRequestsText = "test"
39 |
40 | // When
41 | await self.controller.execute(.newRequestsTextChanged(newValue: dummyNewRequestsText)).value
42 |
43 | // Then
44 | XCTAssertTrue(self.mockStore.isSetRequestsCalled)
45 | }
46 |
47 | func test_quotationRequestButtonTapped() async {
48 | // Given
49 |
50 | // When
51 | await self.controller.execute(.quotationRequestButtonTapped).value
52 |
53 | // Then
54 | XCTAssertTrue(self.mockInteractor.isRequestQuotationCalled)
55 | }
56 | }
57 |
58 | // MARK: - Mock Classes
59 |
60 | extension AddRequestsControllerTests {
61 |
62 | class MockInteractor: AddRequestsInteractable {
63 | var isRequestQuotationCalled = false
64 |
65 | func execute(_ useCase: AddRequestsUseCase) async {
66 | switch useCase {
67 | case .requestQuotation:
68 | self.isRequestQuotationCalled = true
69 | }
70 | }
71 | }
72 |
73 | class MockStore: AddRequestsMutatable {
74 | var isSetRequestsCalled = false
75 |
76 | @MainActor func execute(_ mutation: AddRequestsMutation) {
77 | switch mutation {
78 | case .setRequests:
79 | self.isSetRequestsCalled = true
80 |
81 | default:
82 | break
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/AddRequestsStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsStore.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - Mutation
12 |
13 | enum AddRequestsMutation {
14 | case mutateRequestQuotation(response: AddRequests.RequestQuotation.Response)
15 |
16 | case setRequests(requests: String)
17 | }
18 |
19 | // MARK: - State
20 |
21 | @MainActor public struct AddRequestsState {
22 | var requestsText: String = ""
23 | var message: String = ""
24 |
25 | var domainState: DomainState
26 |
27 | struct DomainState {
28 | var receiptImageUploadUrlObjectKeys: [String]
29 | var requests: String = ""
30 | }
31 |
32 | public init(receiptImageUploadUrlObjectKeys: [String]) {
33 | self.domainState = DomainState(receiptImageUploadUrlObjectKeys: receiptImageUploadUrlObjectKeys)
34 | }
35 | }
36 |
37 | // MARK: - Store
38 |
39 | protocol AddRequestsMutatable: AnyObject {
40 | @MainActor func execute(_ mutation: AddRequestsMutation)
41 | }
42 |
43 | protocol HasAddRequestsDomainState: AnyObject {
44 | @MainActor var domainState: AddRequestsState.DomainState { get }
45 | }
46 |
47 | @MainActor final class AddRequestsStore: AddRequestsMutatable, HasAddRequestsDomainState, ObservableObject {
48 |
49 | let worker: AddRequestsWorkable
50 | @Published private(set) var state: AddRequestsState
51 | var domainState: AddRequestsState.DomainState { self.state.domainState }
52 |
53 | init(
54 | worker: AddRequestsWorkable,
55 | state: AddRequestsState
56 | ) {
57 | self.worker = worker
58 | self.state = state
59 | }
60 |
61 | func execute(_ mutation: AddRequestsMutation) {
62 | self.state = self.execute(state: self.state, mutation: mutation)
63 | }
64 | }
65 |
66 | // MARK: - Implement
67 |
68 | extension AddRequestsStore {
69 |
70 | func execute(state: AddRequestsState, mutation: AddRequestsMutation) -> AddRequestsState {
71 | var state = state
72 |
73 | switch mutation {
74 | case .mutateRequestQuotation(let response):
75 | if let error = response.error {
76 | state.message = error.localizedDescription
77 | }
78 | else {
79 | self.worker.delegate?.quotationRequestSuccessed(successMessage: "견적 요청을 성공하였습니다 :)")
80 | }
81 |
82 | case .setRequests(let requests):
83 | state.domainState.requests = requests
84 | state.requestsText = requests
85 | }
86 |
87 | return state
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Tests/AddRequestsSceneTests/Unit/AddRequestsStoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsStoreTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/23.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import AddRequestsScene
11 |
12 | @MainActor class AddRequestsStoreTests: XCTestCase {
13 |
14 | var store: AddRequestsStore!
15 | @MainActor var state: AddRequestsState { self.store.state }
16 |
17 | var mockWorker: MockWorker!
18 | var mockDelegate: MockDelegate!
19 |
20 | override func setUp() {
21 | super.setUp()
22 |
23 | self.mockWorker = MockWorker()
24 | self.mockDelegate = MockDelegate()
25 | self.mockWorker.delegate = self.mockDelegate
26 | self.configure(initialState: AddRequestsState(receiptImageUploadUrlObjectKeys: [""]))
27 | }
28 |
29 | private func configure(initialState: AddRequestsState) {
30 | self.store = AddRequestsStore(worker: self.mockWorker, state: initialState)
31 | }
32 |
33 | override func tearDown() {
34 | self.store = nil
35 | self.mockWorker = nil
36 | super.tearDown()
37 | }
38 |
39 | // MARK: - Test Cases
40 |
41 | func test_mutateRequestQuotation__error() {
42 | // Given
43 | let dummyResponse = AddRequests.RequestQuotation.Response(error: NSError(domain: "Test_Error", code: -999, userInfo: nil))
44 |
45 | // When
46 | self.store.execute(.mutateRequestQuotation(response: dummyResponse))
47 |
48 | // Then
49 | XCTAssertEqual(self.state.message, "작업을 완료할 수 없습니다.(Test_Error 오류 -999.)")
50 | }
51 |
52 | func test_mutateRequestQuotation__success() {
53 | // Given
54 | let dummyResponse = AddRequests.RequestQuotation.Response(error: nil)
55 |
56 | // When
57 | self.store.execute(.mutateRequestQuotation(response: dummyResponse))
58 |
59 | // Then
60 | XCTAssertEqual(self.mockDelegate.lastSuccessMessage, "견적 요청을 성공하였습니다 :)")
61 | }
62 |
63 | func test_setRequests() {
64 | // Given
65 | let dummyRequests = "test"
66 |
67 | // When
68 | self.store.execute(.setRequests(requests: dummyRequests))
69 |
70 | // Then
71 | XCTAssertEqual(self.state.domainState.requests, dummyRequests)
72 | XCTAssertEqual(self.state.requestsText, dummyRequests)
73 | }
74 | }
75 |
76 | // MARK: - Mock Classes
77 |
78 | extension AddRequestsStoreTests {
79 |
80 | class MockWorker: AddRequestsWorkable {
81 |
82 | var delegate: AddRequestsDelegate?
83 |
84 | func requestCreateQuotation(receiptImageUploadUrlObjectKeys: [String], requests: String) async throws {
85 | return
86 | }
87 | }
88 |
89 | class MockDelegate: AddRequestsDelegate {
90 |
91 | var lastSuccessMessage: String?
92 | func quotationRequestSuccessed(successMessage: String) {
93 | self.lastSuccessMessage = successMessage
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,macos
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,macos
3 |
4 | ### macOS ###
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Icon must end with two \r
11 | Icon
12 |
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### macOS Patch ###
34 | # iCloud generated files
35 | *.icloud
36 |
37 | ### Swift ###
38 | # Xcode
39 | #
40 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
41 |
42 | ## User settings
43 | xcuserdata/
44 |
45 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
46 | *.xcscmblueprint
47 | *.xccheckout
48 |
49 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
50 | build/
51 | DerivedData/
52 | *.moved-aside
53 | *.pbxuser
54 | !default.pbxuser
55 | *.mode1v3
56 | !default.mode1v3
57 | *.mode2v3
58 | !default.mode2v3
59 | *.perspectivev3
60 | !default.perspectivev3
61 |
62 | ## Obj-C/Swift specific
63 | *.hmap
64 |
65 | ## App packaging
66 | *.ipa
67 | *.dSYM.zip
68 | *.dSYM
69 |
70 | ## Playgrounds
71 | timeline.xctimeline
72 | playground.xcworkspace
73 |
74 | # Swift Package Manager
75 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
76 | # Packages/
77 | # Package.pins
78 | # Package.resolved
79 | # *.xcodeproj
80 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
81 | # hence it is not needed unless you have added a package configuration file to your project
82 | # .swiftpm
83 |
84 | .build/
85 |
86 | # CocoaPods
87 | # We recommend against adding the Pods directory to your .gitignore. However
88 | # you should judge for yourself, the pros and cons are mentioned at:
89 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
90 | # Pods/
91 | # Add this line if you want to avoid checking in source code from the Xcode workspace
92 | # *.xcworkspace
93 |
94 | # Carthage
95 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
96 | # Carthage/Checkouts
97 |
98 | Carthage/Build/
99 |
100 | # Accio dependency management
101 | Dependencies/
102 | .accio/
103 |
104 | # fastlane
105 | # It is recommended to not store the screenshots in the git repo.
106 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
107 | # For more information about the recommended setup visit:
108 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
109 |
110 | fastlane/report.xml
111 | fastlane/Preview.html
112 | fastlane/screenshots/**/*.png
113 | fastlane/test_output
114 |
115 | # Code Injection
116 | # After new code Injection tools there's a generated folder /iOSInjectionProject
117 | # https://github.com/johnno1962/injectionforxcode
118 |
119 | iOSInjectionProject/
120 |
121 | ### Xcode ###
122 |
123 | ## Xcode 8 and earlier
124 |
125 | ### Xcode Patch ###
126 | *.xcodeproj/*
127 | !*.xcodeproj/project.pbxproj
128 | !*.xcodeproj/xcshareddata/
129 | !*.xcodeproj/project.xcworkspace/
130 | !*.xcworkspace/contents.xcworkspacedata
131 | /*.gcno
132 | **/xcshareddata/WorkspaceSettings.xcsettings
133 |
134 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,macos
135 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Tests/AddRequestsSceneTests/Unit/AddRequestsInteractorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsInteractorTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/23.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import AddRequestsScene
11 |
12 | class AddRequestsInteractorTests: XCTestCase {
13 |
14 | var interactor: AddRequestsInteractor!
15 |
16 | var mockStore: MockStore!
17 | var mockWorker: MockWorker!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockStore = MockStore()
23 | self.mockWorker = MockWorker()
24 | self.interactor = AddRequestsInteractor(store: self.mockStore, worker: self.mockWorker)
25 | }
26 |
27 | override func tearDown() {
28 | self.interactor = nil
29 | self.mockStore = nil
30 | self.mockWorker = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | // MARK: - Test RequestQuotation UseCase
37 |
38 | func test_requestQuotation__worker_requestCreateQuotation_호출() async {
39 | // Given
40 | let dummyRequest = AddRequests.RequestQuotation.Request()
41 |
42 | // When
43 | await self.interactor.execute(.requestQuotation(request: dummyRequest))
44 |
45 | // Then
46 | XCTAssertTrue(self.mockWorker.isRequestCreateQuotationCalled)
47 | }
48 |
49 | func test_requestQuotation__worker_requestCreateQuotation_성공이면__error_nil_반환() async {
50 | // Given
51 | let dummyRequest = AddRequests.RequestQuotation.Request()
52 | await MainActor.run {
53 | self.mockWorker.requestCreateQuotationError = nil
54 | }
55 |
56 | // When
57 | await self.interactor.execute(.requestQuotation(request: dummyRequest))
58 |
59 | // Then
60 | XCTAssertNil(self.mockStore.mutateRequestQuotationResponse?.error)
61 | }
62 |
63 | func test_requestQuotation__worker_requestCreateQuotation_실패이면__error_반환() async {
64 | // Given
65 | let dummyRequest = AddRequests.RequestQuotation.Request()
66 | await MainActor.run {
67 | self.mockWorker.requestCreateQuotationError = NSError(domain: "Test_Error", code: -999, userInfo: nil)
68 | }
69 |
70 | // When
71 | await self.interactor.execute(.requestQuotation(request: dummyRequest))
72 |
73 | // Then
74 | XCTAssertNotNil(self.mockStore.mutateRequestQuotationResponse?.error)
75 | }
76 | }
77 |
78 | // MARK: - Mock Classes
79 |
80 | extension AddRequestsInteractorTests {
81 |
82 | class MockStore: AddRequestsMutatable, HasAddRequestsDomainState {
83 | @MainActor var domainState = AddRequestsState.DomainState(receiptImageUploadUrlObjectKeys: [])
84 |
85 | var mutateRequestQuotationResponse: AddRequests.RequestQuotation.Response?
86 |
87 | @MainActor func execute(_ mutation: AddRequestsMutation) {
88 | switch mutation {
89 | case .mutateRequestQuotation(let response):
90 | self.mutateRequestQuotationResponse = response
91 |
92 | default:
93 | break
94 | }
95 | }
96 | }
97 |
98 | class MockWorker: AddRequestsWorkable {
99 | var delegate: AddRequestsDelegate?
100 |
101 | var isRequestCreateQuotationCalled = false
102 | var requestCreateQuotationError: Error?
103 |
104 | func requestCreateQuotation(receiptImageUploadUrlObjectKeys: [String], requests: String) async throws {
105 | self.isRequestCreateQuotationCalled = true
106 | if let error = self.requestCreateQuotationError {
107 | throw error
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/UploadReceiptController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptController.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import AddRequestsScene
10 |
11 | import Foundation
12 |
13 | // MARK: - Action
14 |
15 | public enum UploadReceiptAction {
16 | case imageAttachTapped
17 | case imageAttachmentMethodCameraSelected
18 | case imageAttachmentMethodGallerySelected
19 | case cameraCanceled
20 | case cameraPhotoTakenCompleted(image: UploadReceipt.ImageUIDataModel)
21 | case imagePickerCanceled
22 | case imagePicked(image: UploadReceipt.ImageUIDataModel)
23 | case nextButtonTapped
24 | case addRequestsViewIsActiveChanged(isActive: Bool)
25 | }
26 |
27 | // MARK: - Controller
28 |
29 | public protocol UploadReceiptControllerable: AnyObject {
30 | @discardableResult func execute(_ action: UploadReceiptAction) -> Task
31 | }
32 |
33 | final class UploadReceiptController: UploadReceiptControllerable {
34 |
35 | private let interactor: UploadReceiptInteractable
36 | private weak var store: UploadReceiptMutatable?
37 |
38 | init(
39 | interactor: UploadReceiptInteractable,
40 | store: UploadReceiptMutatable
41 | ) {
42 | self.interactor = interactor
43 | self.store = store
44 | }
45 |
46 | @discardableResult public func execute(_ action: UploadReceiptAction) -> Task {
47 | Task { [weak self] in
48 | await self?.execute(action)
49 | }
50 | }
51 | }
52 |
53 | // MARK: - Implement
54 |
55 | extension UploadReceiptController {
56 |
57 | private func execute(_ action: UploadReceiptAction) async {
58 | switch action {
59 | case .imageAttachTapped:
60 | await self.store?.execute(.showSelectImageAttachmentMethodSheet)
61 |
62 | case .imageAttachmentMethodCameraSelected:
63 | await self.store?.execute(.dismissSelectImageAttachmentMethodSheet)
64 | await self.interactor.execute(.showCamera(request: UploadReceipt.ShowCamera.Request()))
65 |
66 | case .imageAttachmentMethodGallerySelected:
67 | await self.store?.execute(.dismissSelectImageAttachmentMethodSheet)
68 | await self.interactor.execute(.showGallery(request: UploadReceipt.ShowGallery.Request()))
69 |
70 | case .cameraCanceled:
71 | await self.store?.execute(.dismissCamera)
72 |
73 | case .cameraPhotoTakenCompleted(let image):
74 | await self.interactor.execute(.saveImage(request: UploadReceipt.SaveImage.Request(imageUIData: image)))
75 | await self.interactor.execute(.attachImage(request: UploadReceipt.AttachImage.Request(imageUIData: image)))
76 | await self.store?.execute(.dismissCamera)
77 |
78 | case .imagePickerCanceled:
79 | await self.store?.execute(.dismissImagePicker)
80 |
81 | case .imagePicked(let image):
82 | await self.interactor.execute(.attachImage(request: UploadReceipt.AttachImage.Request(imageUIData: image)))
83 | await self.store?.execute(.dismissImagePicker)
84 |
85 | case .nextButtonTapped:
86 | await self.interactor.execute(.uploadImage(request: UploadReceipt.UploadImage.Request()))
87 |
88 | case .addRequestsViewIsActiveChanged(let isActive):
89 | await self.store?.execute(.setIsActiveAddRequestsView(isActive: isActive))
90 | }
91 | }
92 | }
93 |
94 | // MARK: - Implement AddRequests Scene Delegate
95 |
96 | extension UploadReceiptController: AddRequestsDelegate {
97 |
98 | @MainActor func quotationRequestSuccessed(successMessage: String) {
99 | self.store?.execute(.setIsActiveAddRequestsView(isActive: false))
100 | self.store?.execute(.showMessage(message: successMessage))
101 | self.store?.execute(.clearAttachedImages)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Tests/AddRequestsSceneTests/AddRequestsSceneTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsSceneTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import AddRequestsScene
11 |
12 | class AddRequestsSceneTests: XCTestCase {
13 |
14 | var controller: AddRequestsController!
15 | @MainActor var state: AddRequestsState { self.store.state }
16 |
17 | var store: AddRequestsStore!
18 | var interactor: AddRequestsInteractor!
19 |
20 | var mockWorker: MockWorker!
21 | var mockDelegate: MockDelegate!
22 |
23 | @MainActor override func setUp() {
24 | super.setUp()
25 |
26 | self.mockWorker = MockWorker()
27 | self.mockDelegate = MockDelegate()
28 | self.mockWorker.delegate = self.mockDelegate
29 | self.configure(initialState: AddRequestsState(receiptImageUploadUrlObjectKeys: [""]))
30 | }
31 |
32 | @MainActor private func configure(initialState: AddRequestsState) {
33 | self.store = AddRequestsStore(worker: self.mockWorker, state: initialState)
34 | self.interactor = AddRequestsInteractor(store: self.store, worker: self.mockWorker)
35 | self.controller = AddRequestsController(interactor: self.interactor, store: self.store)
36 | }
37 |
38 | override func tearDown() {
39 | self.controller = nil
40 | self.store = nil
41 | self.interactor = nil
42 | self.mockWorker = nil
43 | self.mockDelegate = nil
44 | super.tearDown()
45 | }
46 |
47 | // MARK: - Test Scenarios
48 |
49 | func test_요구사항글자변경__요구사항입력됨() async {
50 | // Given
51 |
52 | // When
53 | await self.controller.execute(.newRequestsTextChanged(newValue: "test")).value
54 |
55 |
56 | // Then
57 | let domainRequests = await self.state.domainState.requests
58 | let requestsText = await self.state.requestsText
59 |
60 | XCTAssertEqual(domainRequests, "test")
61 | XCTAssertEqual(requestsText, "test")
62 | }
63 |
64 | func test_견적요청버튼클릭_견적요청성공_성공출력() async {
65 | // Given
66 | await MainActor.run {
67 | self.mockWorker.requestCreateQuotationResult = .success(())
68 | }
69 |
70 | // When
71 | await self.controller.execute(.quotationRequestButtonTapped).value
72 |
73 | // Then
74 | let lastSuccessMessage = self.mockDelegate.lastSuccessMessage
75 |
76 | XCTAssertEqual(lastSuccessMessage, "견적 요청을 성공하였습니다 :)")
77 | }
78 |
79 | func test_견적요청버튼클릭_견적요청실패_실패출력() async {
80 | // Given
81 | await MainActor.run {
82 | self.mockWorker.requestCreateQuotationResult = .failure(NSError(domain: "Test_Error", code: -999))
83 | }
84 |
85 | // When
86 | await self.controller.execute(.quotationRequestButtonTapped).value
87 |
88 | // Then
89 | let message = await self.state.message
90 |
91 | XCTAssertEqual(message, "작업을 완료할 수 없습니다.(Test_Error 오류 -999.)")
92 | }
93 | }
94 |
95 | // MARK: - Mock Classes
96 |
97 | extension AddRequestsSceneTests {
98 |
99 | class MockWorker: AddRequestsWorkable {
100 |
101 | var delegate: AddRequestsDelegate?
102 |
103 | var requestCreateQuotationResult: Result?
104 | func requestCreateQuotation(receiptImageUploadUrlObjectKeys: [String], requests: String) async throws {
105 | if let requestCreateQuotationResult = self.requestCreateQuotationResult {
106 | switch requestCreateQuotationResult {
107 | case .success(let data):
108 | return data
109 | case .failure(let error):
110 | throw error
111 | }
112 | }
113 | XCTFail("requestCreateQuotation()의 결과값을 주입해주어야합니다.")
114 | }
115 | }
116 |
117 | class MockDelegate: AddRequestsDelegate {
118 |
119 | var lastSuccessMessage: String?
120 | func quotationRequestSuccessed(successMessage: String) {
121 | self.lastSuccessMessage = successMessage
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/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, store, worker. and modularize with SPM(Swift Package Manager)
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___View
38 | Description
39 | The view name
40 | Identifier
41 | viewName
42 | Name
43 | View 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___Store
66 | Description
67 | The store name
68 | Identifier
69 | storeName
70 | Name
71 | Store Name:
72 | Required
73 |
74 | Type
75 | static
76 |
77 |
78 | Default
79 | ___VARIABLE_sceneName:identifier___Controller
80 | Description
81 | The controller name
82 | Identifier
83 | controllerName
84 | Name
85 | Controller Name:
86 | Required
87 |
88 | Type
89 | static
90 |
91 |
92 | Default
93 | ___VARIABLE_sceneName:identifier___Worker
94 | Description
95 | The worker name
96 | Identifier
97 | workerName
98 | Name
99 | Worker Name:
100 | Required
101 |
102 | Type
103 | static
104 |
105 |
106 | Default
107 | ___VARIABLE_sceneName:identifier___Models
108 | Description
109 | The models name
110 | Identifier
111 | modelsName
112 | Name
113 | Models Name:
114 | Required
115 |
116 | Type
117 | static
118 |
119 |
120 | Identifier
121 | uiFramework
122 | Required
123 |
124 | Name
125 | UI Framework:
126 | Description
127 | The type of classes to create
128 | Type
129 | popup
130 | Default
131 | SwiftUI
132 | Values
133 |
134 | SwiftUI
135 | UIKit
136 |
137 |
138 |
139 | Platforms
140 |
141 | com.apple.platform.iphoneos
142 |
143 | SortOrder
144 | 0
145 | Summary
146 | This generates a new scene using Uncle Bob's clean architecture.
147 |
148 |
149 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/UploadReceiptWorker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptWorker.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import AddRequestsScene
10 | import NetworkService
11 |
12 | import Toaster
13 | import Photos
14 |
15 | // MARK: - Delegate
16 |
17 | public protocol UploadReceiptDelegate: AnyObject {
18 | }
19 |
20 | // MARK: - Worker
21 |
22 | protocol UploadReceiptWorkable: AnyObject {
23 | var delegate: UploadReceiptDelegate? { get set }
24 | var addRequestsController: AddRequestsControllerable? { get set }
25 |
26 | @MainActor func showToast(message: String)
27 |
28 | func mapToData(from imageUIData: UploadReceipt.ImageUIDataModel) -> Data?
29 | func fetchImageUploadUrls(names: [String]) async throws -> [UploadReceipt.ImageUploadUrl]
30 | func requestImagesUpload(attatchedImages: [UploadReceipt.ImageModel], imageUploadUrls: [UploadReceipt.ImageUploadUrl]) async throws
31 | func requestCameraPermission() async -> Bool
32 | func requestGalleryPermission() async -> Bool
33 | func saveImage(imageUIData: UploadReceipt.ImageUIDataModel) async throws
34 | }
35 |
36 | final class UploadReceiptWorker: UploadReceiptWorkable {
37 |
38 | weak var delegate: UploadReceiptDelegate?
39 | weak var addRequestsController: AddRequestsControllerable?
40 |
41 | private let imageUploadNetworkService: ImageUploadNetworkServiceProtocol
42 |
43 | init(
44 | delegate: UploadReceiptDelegate,
45 | imageUploadNetworkService: ImageUploadNetworkServiceProtocol
46 | ) {
47 | self.delegate = delegate
48 | self.imageUploadNetworkService = imageUploadNetworkService
49 | }
50 | }
51 |
52 | // MARK: - Implement
53 |
54 | extension UploadReceiptWorker {
55 |
56 | func mapToData(from imageUIData: UploadReceipt.ImageUIDataModel) -> Data? {
57 | return imageUIData.jpegData(compressionQuality: 1.0)
58 | }
59 |
60 | func fetchImageUploadUrls(names: [String]) async throws -> [UploadReceipt.ImageUploadUrl] {
61 | let dto = try await self.imageUploadNetworkService.queryImageUploadUrls(fileNames: names.map({ $0.appending(".jpg")}))
62 | return dto.uploadUrls.map({
63 | UploadReceipt.ImageUploadUrl(uploadUrl: $0.uploadUrl, objectKey: $0.objectKey)
64 | })
65 | }
66 |
67 | func requestImagesUpload(attatchedImages: [UploadReceipt.ImageModel], imageUploadUrls: [UploadReceipt.ImageUploadUrl]) async throws {
68 | return try await withThrowingTaskGroup(of: Void.self) { group in
69 | for (imageUploadUrl, attachedImage) in zip(imageUploadUrls, attatchedImages) {
70 | group.addTask {
71 | try await self.imageUploadNetworkService.putImageData(to: imageUploadUrl.uploadUrl, imageData: attachedImage.data)
72 | }
73 | }
74 | try await group.waitForAll()
75 | }
76 | }
77 |
78 | func requestCameraPermission() async -> Bool {
79 | return await withCheckedContinuation { continuation in
80 | AVCaptureDevice.requestAccess(for: .video) { granted in
81 | continuation.resume(returning: granted)
82 | }
83 | }
84 | }
85 |
86 | func requestGalleryPermission() async -> Bool {
87 | return await withCheckedContinuation { continuation in
88 | if #available(iOS 14, *) {
89 | PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
90 | continuation.resume(returning: status == .authorized)
91 | }
92 | } else {
93 | PHPhotoLibrary.requestAuthorization { status in
94 | continuation.resume(returning: status == .authorized)
95 | }
96 | }
97 | }
98 | }
99 |
100 | func saveImage(imageUIData: UploadReceipt.ImageUIDataModel) async throws {
101 | return try await withCheckedThrowingContinuation { continuation in
102 | PHPhotoLibrary.shared().performChanges({
103 | PHAssetChangeRequest.creationRequestForAsset(from: imageUIData)
104 | }) { success, error in
105 | if let error = error {
106 | continuation.resume(throwing: error)
107 | return
108 | }
109 | continuation.resume()
110 | }
111 | }
112 | }
113 |
114 | @MainActor func showToast(message: String) {
115 | Toast(text: message).show()
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Example/Example/Scene/AddRequestsScene/Sources/AddRequestsScene/View/Any/AddRequestsUIKitView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRequestsUIKitView.swift
3 | //
4 | //
5 | // Created by 박건우 on 2024/01/16.
6 | //
7 |
8 | import NetworkService
9 |
10 | import UIKit
11 | import Combine
12 |
13 | public final class AddRequestsUIKitView: UIViewController {
14 | public typealias ActiveClosure = (_ isActive: Bool) -> Void
15 |
16 | private var cancellables: Set = []
17 |
18 | private let controller: AddRequestsControllerable
19 | private var store: AddRequestsStore
20 | private let activeClosure: ActiveClosure
21 |
22 | public init(
23 | initialState: AddRequestsState,
24 | controller: inout AddRequestsControllerable?,
25 | delegate: AddRequestsDelegate,
26 | activeClosure: @escaping ActiveClosure
27 | ) {
28 | let quotationNetworkService = QuotationNetworkService()
29 | let worker = AddRequestsWorker(delegate: delegate, quotationNetworkService: quotationNetworkService)
30 | let store = AddRequestsStore(worker: worker, state: initialState)
31 | let interactor = AddRequestsInteractor(store: store, worker: worker)
32 | let _controller = AddRequestsController(interactor: interactor, store: store)
33 | controller = _controller
34 |
35 | self.controller = _controller
36 | self.store = store
37 | self.activeClosure = activeClosure
38 |
39 | super.init(nibName: nil, bundle: nil)
40 |
41 | self.bind()
42 | }
43 |
44 | required init?(coder aDecoder: NSCoder) {
45 | fatalError("init(coder:) has not been implemented")
46 | }
47 |
48 | deinit {
49 | self.activeClosure(false)
50 | }
51 |
52 | // MARK: - UI
53 |
54 | private var textField = UITextField()
55 | private var requestButton = UIButton()
56 | private var messageLabel = UILabel()
57 |
58 | // MARK: - View lifecycle
59 |
60 | public override func viewDidLoad() {
61 | super.viewDidLoad()
62 |
63 | self.setupUI()
64 | }
65 |
66 | // MARK: - Layout
67 |
68 | private func setupUI() {
69 | self.view.backgroundColor = .white
70 |
71 | // Navigation Setup
72 | self.title = "견적 요청 - 요청 사항"
73 | self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
74 |
75 | // TextField Setup
76 | self.textField.translatesAutoresizingMaskIntoConstraints = false
77 | self.textField.delegate = self
78 | self.textField.borderStyle = .roundedRect
79 | self.textField.placeholder = "요청 사항 입력"
80 | self.view.addSubview(self.textField)
81 |
82 | // Request Button Setup
83 | self.requestButton.translatesAutoresizingMaskIntoConstraints = false
84 | self.requestButton.setTitle("견적 요청", for: .normal)
85 | self.requestButton.backgroundColor = .blue
86 | self.requestButton.layer.cornerRadius = 10
87 | self.requestButton.addTarget(self, action: #selector(self.requestButtonTapped), for: .touchUpInside)
88 | self.view.addSubview(self.requestButton)
89 |
90 | // Message Label Setup
91 | self.messageLabel.translatesAutoresizingMaskIntoConstraints = false
92 | self.view.addSubview(self.messageLabel)
93 |
94 | // Auto Layout Constraints
95 | NSLayoutConstraint.activate([
96 | self.textField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 20),
97 | self.textField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
98 | self.textField.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
99 |
100 | self.requestButton.topAnchor.constraint(equalTo: self.textField.bottomAnchor, constant: 20),
101 | self.requestButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
102 | self.requestButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
103 | self.requestButton.heightAnchor.constraint(equalToConstant: 50),
104 |
105 | self.messageLabel.topAnchor.constraint(equalTo: self.requestButton.bottomAnchor, constant: 20),
106 | self.messageLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
107 | self.messageLabel.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10)
108 | ])
109 | }
110 |
111 | private func bind() {
112 |
113 | self.store.$state
114 | .map { $0.requestsText }
115 | .sink { requestsText in
116 | _ = requestsText
117 | }
118 | .store(in: &self.cancellables)
119 |
120 | self.store.$state
121 | .map { $0.message }
122 | .sink { [weak self] message in
123 | self?.messageLabel.text = message
124 | }
125 | .store(in: &self.cancellables)
126 | }
127 |
128 | // MARK: - Action Selector
129 |
130 | @objc private func requestButtonTapped() {
131 | self.controller.execute(.quotationRequestButtonTapped)
132 | }
133 | }
134 |
135 | extension AddRequestsUIKitView: UITextFieldDelegate {
136 |
137 | public func textFieldDidChangeSelection(_ textField: UITextField) {
138 | self.controller.execute(.newRequestsTextChanged(newValue: textField.text ?? ""))
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/UploadReceiptInteractor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptInteractor.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - UseCase
12 |
13 | enum UploadReceiptUseCase {
14 | case attachImage(request: UploadReceipt.AttachImage.Request)
15 | case uploadImage(request: UploadReceipt.UploadImage.Request)
16 | case saveImage(request: UploadReceipt.SaveImage.Request)
17 | case showCamera(request: UploadReceipt.ShowCamera.Request)
18 | case showGallery(request: UploadReceipt.ShowGallery.Request)
19 | }
20 |
21 | // MARK: - Interactor
22 |
23 | protocol UploadReceiptInteractable {
24 | func execute(_ useCase: UploadReceiptUseCase) async
25 | }
26 |
27 | final class UploadReceiptInteractor: UploadReceiptInteractable {
28 |
29 | private let store: UploadReceiptMutatable & HasUploadReceiptDomainState
30 | private let worker: UploadReceiptWorkable
31 |
32 | init(
33 | store: UploadReceiptMutatable & HasUploadReceiptDomainState,
34 | worker: UploadReceiptWorkable
35 | ) {
36 | self.store = store
37 | self.worker = worker
38 | }
39 | }
40 |
41 | // MARK: - Implement
42 |
43 | extension UploadReceiptInteractor {
44 |
45 | func execute(_ useCase: UploadReceiptUseCase) async {
46 | switch useCase {
47 | case .attachImage(let request):
48 | let response: UploadReceipt.AttachImage.Response
49 | let storedAttachedImages = await self.store.domainState.attachedImages
50 |
51 | if storedAttachedImages.count >= 5 {
52 | response = UploadReceipt.AttachImage.Response(
53 | attachedImages: storedAttachedImages,
54 | error: .exceedImageCount
55 | )
56 | }
57 | else if let imageData = self.worker.mapToData(from: request.imageUIData) {
58 | response = UploadReceipt.AttachImage.Response(
59 | attachedImages: storedAttachedImages + [UploadReceipt.ImageModel(data: imageData, uiData: request.imageUIData)],
60 | error: nil
61 | )
62 | }
63 | else {
64 | response = UploadReceipt.AttachImage.Response(
65 | attachedImages: storedAttachedImages,
66 | error: .failDataMapping
67 | )
68 | }
69 |
70 | await self.store.execute(.mutateAttachImage(response: response))
71 |
72 | case .uploadImage(_):
73 | let response: UploadReceipt.UploadImage.Response
74 | let storedAttachedImages = await self.store.domainState.attachedImages
75 |
76 | if storedAttachedImages.isEmpty {
77 | response = UploadReceipt.UploadImage.Response(
78 | uploadUrlObjectKeys: [],
79 | error: .emptyAttachedImage
80 | )
81 | }
82 | else {
83 | do {
84 | let uploadUrls = try await self.worker.fetchImageUploadUrls(names: storedAttachedImages.map({ $0.name }))
85 | try await self.worker.requestImagesUpload(attatchedImages: storedAttachedImages, imageUploadUrls: uploadUrls)
86 |
87 | response = UploadReceipt.UploadImage.Response(
88 | uploadUrlObjectKeys: uploadUrls.map({ $0.objectKey }),
89 | error: nil
90 | )
91 | }
92 | catch {
93 | response = UploadReceipt.UploadImage.Response(
94 | uploadUrlObjectKeys: [],
95 | error: .default(error)
96 | )
97 | }
98 | }
99 |
100 | await self.store.execute(.mutateUploadImage(response: response))
101 |
102 | case .saveImage(let request):
103 | let response: UploadReceipt.SaveImage.Response
104 |
105 | do {
106 | try await self.worker.saveImage(imageUIData: request.imageUIData)
107 | response = UploadReceipt.SaveImage.Response(error: nil)
108 | }
109 | catch {
110 | response = UploadReceipt.SaveImage.Response(error: error)
111 | }
112 |
113 | await self.store.execute(.mutateSaveImage(response: response))
114 |
115 | case .showCamera(_):
116 | let response: UploadReceipt.ShowCamera.Response
117 |
118 | let cameraPermissionAccepted = await self.worker.requestCameraPermission()
119 | let galleryPermissionAccepted = await self.worker.requestGalleryPermission()
120 | let showingCameraPermissionAccepted = cameraPermissionAccepted && galleryPermissionAccepted
121 | response = UploadReceipt.ShowCamera.Response(permissionDenied: !showingCameraPermissionAccepted)
122 |
123 | await self.store.execute(.mutateShowCamera(response: response))
124 |
125 | case .showGallery(_):
126 | let response: UploadReceipt.ShowGallery.Response
127 |
128 | let galleryPermissionAccepted = await self.worker.requestGalleryPermission()
129 | let showingGalleryPermissionAccepted = galleryPermissionAccepted
130 | response = UploadReceipt.ShowGallery.Response(permissionDenied: !showingGalleryPermissionAccepted)
131 |
132 | await self.store.execute(.mutateShowGallery(response: response))
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/UploadReceiptStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptStore.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import AddRequestsScene
10 |
11 | import Foundation
12 |
13 | // MARK: - Mutation
14 |
15 | enum UploadReceiptMutation {
16 | case mutateAttachImage(response: UploadReceipt.AttachImage.Response)
17 | case mutateUploadImage(response: UploadReceipt.UploadImage.Response)
18 | case mutateSaveImage(response: UploadReceipt.SaveImage.Response)
19 | case mutateShowCamera(response: UploadReceipt.ShowCamera.Response)
20 | case mutateShowGallery(response: UploadReceipt.ShowGallery.Response)
21 |
22 | case showSelectImageAttachmentMethodSheet
23 | case dismissSelectImageAttachmentMethodSheet
24 | case dismissCamera
25 | case dismissImagePicker
26 | case showMessage(message: String)
27 | case clearAttachedImages
28 | case setIsActiveAddRequestsView(isActive: Bool)
29 | }
30 |
31 | // MARK: - State
32 |
33 | @MainActor public struct UploadReceiptState {
34 | var attachedImages: [UploadReceipt.ImageUIDataModel] = []
35 | var showingSelectImageAttachmentMethodSheet: Bool = false
36 | var showingImagePicker: Bool = false
37 | var showingCamera: Bool = false
38 | var isAddRequestsViewActive: Bool = false
39 | var receiptImageUploadUrlObjectKeys: [String] = []
40 |
41 | var domainState = DomainState()
42 |
43 | struct DomainState {
44 | var attachedImages: [UploadReceipt.ImageModel] = []
45 | }
46 |
47 | public init() {}
48 | }
49 |
50 | // MARK: - Store
51 |
52 | protocol UploadReceiptMutatable: AnyObject {
53 | @MainActor func execute(_ mutation: UploadReceiptMutation)
54 | }
55 |
56 | protocol HasUploadReceiptDomainState: AnyObject {
57 | @MainActor var domainState: UploadReceiptState.DomainState { get }
58 | }
59 |
60 | @MainActor final class UploadReceiptStore: UploadReceiptMutatable, HasUploadReceiptDomainState, ObservableObject {
61 |
62 | let worker: UploadReceiptWorkable
63 | @Published private(set) var state: UploadReceiptState
64 | var domainState: UploadReceiptState.DomainState { self.state.domainState }
65 |
66 | init(
67 | worker: UploadReceiptWorkable,
68 | state: UploadReceiptState
69 | ) {
70 | self.worker = worker
71 | self.state = state
72 | }
73 |
74 | func execute(_ mutation: UploadReceiptMutation) {
75 | self.state = self.execute(state: self.state, mutation: mutation)
76 | }
77 | }
78 |
79 | // MARK: - Implement
80 | extension UploadReceiptStore {
81 |
82 | private func execute(state: UploadReceiptState, mutation: UploadReceiptMutation) -> UploadReceiptState {
83 | var state = state
84 |
85 | switch mutation {
86 | case .mutateAttachImage(let response):
87 | if let error = response.error {
88 | switch error {
89 | case .exceedImageCount:
90 | self.worker.showToast(message: "최대 5장까지 첨부할 수 있습니다")
91 |
92 | case .failDataMapping:
93 | self.worker.showToast(message: "사진 처리 과정에서 오류가 발생하였습니다")
94 | }
95 | }
96 | else {
97 | state.domainState.attachedImages = response.attachedImages
98 | state.attachedImages = response.attachedImages.map { $0.uiData }
99 | }
100 |
101 | case .mutateUploadImage(let response):
102 | if let error = response.error {
103 | switch error {
104 | case .emptyAttachedImage:
105 | self.worker.showToast(message: "견적 요청을 하기 위해서\n필수로 이미지를 첨부하여야 합니다")
106 |
107 | case .default(let error):
108 | self.worker.showToast(message: error.localizedDescription)
109 | }
110 | }
111 | else {
112 | state.receiptImageUploadUrlObjectKeys = response.uploadUrlObjectKeys // Data Passing
113 | state.isAddRequestsViewActive = true // Active AddRequests View
114 | }
115 |
116 | case .mutateSaveImage(let response):
117 | if let error = response.error {
118 | self.worker.showToast(message: error.localizedDescription)
119 | }
120 |
121 | case .mutateShowCamera(let response):
122 | if response.permissionDenied {
123 | self.worker.showToast(message: "명세표를 업로드하기 위해서\n카메라 및 모든 사진 접근 권한이 필요합니다")
124 | }
125 | state.showingCamera = !response.permissionDenied
126 |
127 | case .mutateShowGallery(let response):
128 | if response.permissionDenied {
129 | self.worker.showToast(message: "명세표를 업로드하기 위해서\n 모든 사진 접근 권한이 필요합니다")
130 | }
131 | state.showingImagePicker = !response.permissionDenied
132 |
133 | case .showSelectImageAttachmentMethodSheet:
134 | state.showingSelectImageAttachmentMethodSheet = true
135 |
136 | case .dismissSelectImageAttachmentMethodSheet:
137 | state.showingSelectImageAttachmentMethodSheet = false
138 |
139 | case .dismissCamera:
140 | state.showingCamera = false
141 |
142 | case .dismissImagePicker:
143 | state.showingImagePicker = false
144 |
145 | case .showMessage(let message):
146 | self.worker.showToast(message: message)
147 |
148 | case .clearAttachedImages:
149 | state.attachedImages = []
150 | state.domainState.attachedImages = []
151 |
152 | case .setIsActiveAddRequestsView(let isActive):
153 | state.isAddRequestsViewActive = isActive
154 | }
155 |
156 | return state
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Templates/Clean Swift - CIS/Scene-Package.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, store, worker, scene tests, controller tests, interactor tests, store tests. and modularize with SPM(Swift Package Manager)
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___View
38 | Description
39 | The view name
40 | Identifier
41 | viewName
42 | Name
43 | View 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___Store
66 | Description
67 | The store name
68 | Identifier
69 | storeName
70 | Name
71 | Store Name:
72 | Required
73 |
74 | Type
75 | static
76 |
77 |
78 | Default
79 | ___VARIABLE_sceneName:identifier___Controller
80 | Description
81 | The controller name
82 | Identifier
83 | controllerName
84 | Name
85 | Controller Name:
86 | Required
87 |
88 | Type
89 | static
90 |
91 |
92 | Default
93 | ___VARIABLE_sceneName:identifier___Worker
94 | Description
95 | The worker name
96 | Identifier
97 | workerName
98 | Name
99 | Worker Name:
100 | Required
101 |
102 | Type
103 | static
104 |
105 |
106 | Default
107 | ___VARIABLE_sceneName:identifier___Models
108 | Description
109 | The models name
110 | Identifier
111 | modelsName
112 | Name
113 | Models Name:
114 | Required
115 |
116 | Type
117 | static
118 |
119 |
120 | Default
121 | ___VARIABLE_sceneName:identifier___SceneTests
122 | Description
123 | The sceneTests name
124 | Identifier
125 | sceneTestsName
126 | Name
127 | SceneTests Name:
128 | Required
129 |
130 | Type
131 | static
132 |
133 |
134 | Default
135 | ___VARIABLE_sceneName:identifier___ControllerTests
136 | Description
137 | The controllerTests name
138 | Identifier
139 | controllerTestsName
140 | Name
141 | ControllerTests Name:
142 | Required
143 |
144 | Type
145 | static
146 |
147 |
148 | Default
149 | ___VARIABLE_sceneName:identifier___InteractorTests
150 | Description
151 | The interactorTests name
152 | Identifier
153 | interactorTestsName
154 | Name
155 | interactorTests Name:
156 | Required
157 |
158 | Type
159 | static
160 |
161 |
162 | Default
163 | ___VARIABLE_sceneName:identifier___StoreTests
164 | Description
165 | The storeTests name
166 | Identifier
167 | storeTestsName
168 | Name
169 | StoreTests Name:
170 | Required
171 |
172 | Type
173 | static
174 |
175 |
176 | Identifier
177 | uiFramework
178 | Required
179 |
180 | Name
181 | UI Framework:
182 | Description
183 | The type of classes to create
184 | Type
185 | popup
186 | Default
187 | SwiftUI
188 | Values
189 |
190 | SwiftUI
191 | UIKit
192 |
193 |
194 |
195 | Platforms
196 |
197 | com.apple.platform.iphoneos
198 |
199 | SortOrder
200 | 0
201 | Summary
202 | This generates a new scene using Uncle Bob's clean architecture.
203 |
204 |
205 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Tests/UploadReceiptSceneTests/Unit/UploadReceiptControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptControllerTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/23.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import UploadReceiptScene
11 |
12 | class UploadReceiptControllerTests: XCTestCase {
13 |
14 | var controller: UploadReceiptController!
15 |
16 | var mockInteractor: MockInteractor!
17 | var mockStore: MockStore!
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | self.mockInteractor = MockInteractor()
23 | self.mockStore = MockStore()
24 | self.controller = UploadReceiptController(interactor: self.mockInteractor, store: self.mockStore)
25 | }
26 |
27 | override func tearDown() {
28 | self.controller = nil
29 | self.mockInteractor = nil
30 | self.mockStore = nil
31 | super.tearDown()
32 | }
33 |
34 | // MARK: - Test Cases
35 |
36 | func test_imageAttachTapped() async {
37 | // Given
38 |
39 | // When
40 | await self.controller.execute(.imageAttachTapped).value
41 |
42 | // Then
43 | XCTAssertTrue(self.mockStore.isShowSelectImageAttachmentMethodSheetCalled)
44 | }
45 |
46 | func test_imageAttachmentMethodCameraSelected() async {
47 | // Given
48 |
49 | // When
50 | await self.controller.execute(.imageAttachmentMethodCameraSelected).value
51 |
52 | // Then
53 | XCTAssertTrue(self.mockStore.isDismissSelectImageAttachmentMethodSheetCalled)
54 | XCTAssertTrue(self.mockInteractor.isShowCameraCalled)
55 | }
56 |
57 | func test_imageAttachmentMethodGallerySelected() async {
58 | // Given
59 |
60 | // When
61 | await self.controller.execute(.imageAttachmentMethodGallerySelected).value
62 |
63 | // Then
64 | XCTAssertTrue(self.mockStore.isDismissSelectImageAttachmentMethodSheetCalled)
65 | XCTAssertTrue(self.mockInteractor.isShowGalleryCalled)
66 | }
67 |
68 | func test_cameraCanceled() async {
69 | // Given
70 |
71 | // When
72 | await self.controller.execute(.cameraCanceled).value
73 |
74 | // Then
75 | XCTAssertTrue(self.mockStore.isDismissCameraCalled)
76 | }
77 |
78 | func test_cameraPhotoTakenCompleted() async {
79 | // Given
80 | let dummyImage = UploadReceipt.ImageUIDataModel()
81 |
82 | // When
83 | await self.controller.execute(.cameraPhotoTakenCompleted(image: dummyImage)).value
84 |
85 | // Then
86 | XCTAssertTrue(self.mockInteractor.isSaveImageCalled)
87 | XCTAssertTrue(self.mockInteractor.isAttachImageCalled)
88 | XCTAssertTrue(self.mockStore.isDismissCameraCalled)
89 | }
90 |
91 | func test_imagePickerCanceled() async {
92 | // Given
93 |
94 | // When
95 | await self.controller.execute(.imagePickerCanceled).value
96 |
97 | // Then
98 | XCTAssertTrue(self.mockStore.isDismissImagePickerCalled)
99 | }
100 |
101 | func test_imagePicked() async {
102 | // Given
103 | let dummyImage = UploadReceipt.ImageUIDataModel()
104 |
105 | // When
106 | await self.controller.execute(.imagePicked(image: dummyImage)).value
107 |
108 | // Then
109 | XCTAssertTrue(self.mockInteractor.isAttachImageCalled)
110 | XCTAssertTrue(self.mockStore.isDismissImagePickerCalled)
111 | }
112 |
113 | func test_nextButtonTapped() async {
114 | // Given
115 |
116 | // When
117 | await self.controller.execute(.nextButtonTapped).value
118 |
119 | // Then
120 | XCTAssertTrue(self.mockInteractor.isUploadImageCalled)
121 | }
122 |
123 | func test_addRequestsViewIsActiveChanged() async {
124 | // Given
125 |
126 | // When
127 | await self.controller.execute(.addRequestsViewIsActiveChanged(isActive: true)).value
128 |
129 | // Then
130 | XCTAssertTrue(self.mockStore.isSetActiveAddRequestsViewCalled)
131 | }
132 |
133 | }
134 |
135 | // MARK: - Mock Classes
136 |
137 | extension UploadReceiptControllerTests {
138 |
139 | class MockInteractor: UploadReceiptInteractable {
140 | var isShowCameraCalled = false
141 | var isShowGalleryCalled = false
142 | var isSaveImageCalled = false
143 | var isAttachImageCalled = false
144 | var isUploadImageCalled = false
145 |
146 | func execute(_ useCase: UploadReceiptUseCase) async {
147 | switch useCase {
148 | case .showCamera:
149 | self.isShowCameraCalled = true
150 |
151 | case .showGallery:
152 | self.isShowGalleryCalled = true
153 |
154 | case .saveImage:
155 | self.isSaveImageCalled = true
156 |
157 | case .attachImage:
158 | self.isAttachImageCalled = true
159 |
160 | case .uploadImage:
161 | self.isUploadImageCalled = true
162 | }
163 | }
164 | }
165 |
166 | class MockStore: UploadReceiptMutatable {
167 | var isShowSelectImageAttachmentMethodSheetCalled = false
168 | var isDismissSelectImageAttachmentMethodSheetCalled = false
169 | var isDismissCameraCalled = false
170 | var isDismissImagePickerCalled = false
171 | var isSetActiveAddRequestsViewCalled = false
172 |
173 | @MainActor func execute(_ mutation: UploadReceiptMutation) {
174 | switch mutation {
175 | case .showSelectImageAttachmentMethodSheet:
176 | self.isShowSelectImageAttachmentMethodSheetCalled = true
177 |
178 | case .dismissSelectImageAttachmentMethodSheet:
179 | self.isDismissSelectImageAttachmentMethodSheetCalled = true
180 |
181 | case .dismissCamera:
182 | self.isDismissCameraCalled = true
183 |
184 | case .dismissImagePicker:
185 | self.isDismissImagePickerCalled = true
186 |
187 | case .setIsActiveAddRequestsView:
188 | self.isSetActiveAddRequestsViewCalled = true
189 |
190 | default:
191 | break
192 | }
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/View/Any/UploadReceiptSwiftUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptView.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2023/12/21.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import AddRequestsScene
10 | import NetworkService
11 |
12 | import SwiftUI
13 |
14 | public struct UploadReceiptSwiftUIView: View {
15 |
16 | private let controller: (UploadReceiptControllerable & AddRequestsDelegate)
17 | @ObservedObject private var store: UploadReceiptStore
18 |
19 | public init(
20 | initialState: UploadReceiptState,
21 | controller: inout UploadReceiptControllerable?,
22 | delegate: UploadReceiptDelegate
23 | ) {
24 | let imageUploadNetworkService = ImageUploadNetworkService()
25 | let worker = UploadReceiptWorker(delegate: delegate, imageUploadNetworkService: imageUploadNetworkService)
26 | let store = UploadReceiptStore(worker: worker, state: initialState)
27 | let interactor = UploadReceiptInteractor(store: store, worker: worker)
28 | let _controller = UploadReceiptController(interactor: interactor, store: store)
29 | controller = _controller
30 |
31 | self.controller = _controller
32 | self.store = store
33 | }
34 |
35 | public var body: some View {
36 | NavigationView {
37 | VStack {
38 | ScrollView(.horizontal, showsIndicators: false) {
39 | HStack {
40 | ForEach(self.store.state.attachedImages, id: \.self) { image in
41 | Image(uiImage: image)
42 | .resizable()
43 | .frame(width: 100, height: 100)
44 | .cornerRadius(10)
45 | .padding(.trailing, 10)
46 | }
47 | }
48 | }
49 | .frame(maxWidth: .infinity)
50 | .padding(.horizontal, 10)
51 | .padding(.top, 20)
52 | .padding(.bottom, 10)
53 |
54 | Text("사진 첨부")
55 | .foregroundColor(.blue)
56 | .frame(maxWidth: .infinity, minHeight: 50)
57 | .cornerRadius(10)
58 | .padding(.horizontal, 10)
59 | .overlay(
60 | RoundedRectangle(cornerRadius: 10)
61 | .stroke(.blue, lineWidth: 1)
62 | .padding(.horizontal, 10)
63 | )
64 | .onTapGesture {
65 | self.controller.execute(.imageAttachTapped)
66 | }
67 |
68 | Text("다음")
69 | .foregroundColor(.white)
70 | .frame(maxWidth: .infinity, minHeight: 50)
71 | .background(Color.blue)
72 | .cornerRadius(10)
73 | .padding(.horizontal, 10)
74 | .onTapGesture {
75 | self.controller.execute(.nextButtonTapped)
76 | }
77 |
78 | Spacer()
79 |
80 | NavigationLink(
81 | isActive: Binding(
82 | get: {
83 | return self.store.state.isAddRequestsViewActive
84 | },
85 | set: { isActive in
86 | self.controller.execute(.addRequestsViewIsActiveChanged(isActive: isActive))
87 | }
88 | ),
89 | destination: {
90 | AddRequestsSwiftUIView(
91 | initialState: AddRequestsState(
92 | receiptImageUploadUrlObjectKeys: self.store.state.receiptImageUploadUrlObjectKeys
93 | ),
94 | controller: &self.store.worker.addRequestsController,
95 | delegate: self.controller
96 | )
97 | }
98 | ) {
99 | EmptyView()
100 | }
101 | }
102 | .navigationBarTitle("견적요청 - 명세표 업로드", displayMode: .inline)
103 | .actionSheet(isPresented: Binding(
104 | get: {
105 | return self.store.state.showingSelectImageAttachmentMethodSheet
106 | },
107 | set: { _ in
108 |
109 | }
110 | )) {
111 | ActionSheet(
112 | title: Text("사진 추가하기"),
113 | buttons: [
114 | .default(Text("사진 촬영하기")) {
115 | self.controller.execute(.imageAttachmentMethodCameraSelected)
116 | },
117 | .default(Text("사진첩에서 가져오기")) {
118 | self.controller.execute(.imageAttachmentMethodGallerySelected)
119 | },
120 | .cancel()
121 | ]
122 | )
123 | }
124 | .sheet(isPresented: Binding(
125 | get: {
126 | return self.store.state.showingImagePicker
127 | },
128 | set: { _ in
129 |
130 | }
131 | )) {
132 | ImagePickerController(controller: self.controller, sourceType: .photoLibrary)
133 | }
134 | .sheet(isPresented: Binding(
135 | get: {
136 | return self.store.state.showingCamera
137 | },
138 | set: { _ in
139 |
140 | }
141 | )) {
142 | ImagePickerController(controller: self.controller, sourceType: .camera)
143 | }
144 | }
145 | }
146 | }
147 |
148 | struct ImagePickerController: UIViewControllerRepresentable {
149 |
150 | weak var controller: UploadReceiptControllerable?
151 |
152 | var sourceType: UIImagePickerController.SourceType
153 |
154 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
155 | weak var controller: UploadReceiptControllerable?
156 |
157 | init(_ controller: UploadReceiptControllerable?) {
158 | self.controller = controller
159 | }
160 |
161 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
162 | guard let image = info[.originalImage] as? UIImage else {
163 | return
164 | }
165 | switch picker.sourceType {
166 | case .camera:
167 | self.controller?.execute(.cameraPhotoTakenCompleted(image: image))
168 |
169 | case .photoLibrary:
170 | self.controller?.execute(.imagePicked(image: image))
171 |
172 | default:
173 | break
174 | }
175 | }
176 |
177 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
178 | switch picker.sourceType {
179 | case .camera:
180 | self.controller?.execute(.cameraCanceled)
181 |
182 | case .photoLibrary:
183 | self.controller?.execute(.imagePickerCanceled)
184 |
185 | default:
186 | break
187 | }
188 | }
189 | }
190 |
191 | func makeCoordinator() -> Coordinator {
192 | Coordinator(self.controller)
193 | }
194 |
195 | func makeUIViewController(context: Context) -> UIViewController {
196 | let picker = UIImagePickerController()
197 | picker.delegate = context.coordinator
198 | picker.sourceType = sourceType
199 | return picker
200 | }
201 |
202 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
203 |
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Tests/UploadReceiptSceneTests/Unit/UploadReceiptStoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptStoreTests.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/23.
6 | // Copyright (c) 2023 Spoqa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import AddRequestsScene
11 | @testable import UploadReceiptScene
12 |
13 | @MainActor class UploadReceiptStoreTests: XCTestCase {
14 |
15 | var store: UploadReceiptStore!
16 | @MainActor var state: UploadReceiptState { self.store.state }
17 |
18 | var mockWorker: MockWorker!
19 |
20 | override func setUp() {
21 | super.setUp()
22 |
23 | self.mockWorker = MockWorker()
24 | self.configure(initialState: UploadReceiptState())
25 | }
26 |
27 | private func configure(initialState: UploadReceiptState) {
28 | self.store = UploadReceiptStore(worker: self.mockWorker, state: initialState)
29 | }
30 |
31 | override func tearDown() {
32 | self.store = nil
33 | self.mockWorker = nil
34 | super.tearDown()
35 | }
36 |
37 | // MARK: - Test Cases
38 |
39 | func test_mutateAttachImage__error_exceedImageCount() {
40 | // Given
41 | let dummyResponse = UploadReceipt.AttachImage.Response(attachedImages: [], error: .exceedImageCount)
42 |
43 | // When
44 | self.store.execute(.mutateAttachImage(response: dummyResponse))
45 |
46 | // Then
47 | XCTAssertEqual(self.mockWorker.showToastMessage, "최대 5장까지 첨부할 수 있습니다")
48 | }
49 |
50 | func test_mutateAttachImage__error_failDataMapping() {
51 | // Given
52 | let dummyResponse = UploadReceipt.AttachImage.Response(attachedImages: [], error: .failDataMapping)
53 |
54 | // When
55 | self.store.execute(.mutateAttachImage(response: dummyResponse))
56 |
57 | // Then
58 | XCTAssertEqual(self.mockWorker.showToastMessage, "사진 처리 과정에서 오류가 발생하였습니다")
59 | }
60 |
61 | func test_mutateAttachImage__success() {
62 | // Given
63 | let dummyAttachedImages = [UploadReceipt.ImageModel(data: Data(), uiData: UploadReceipt.ImageUIDataModel())]
64 | let dummyResponse = UploadReceipt.AttachImage.Response(attachedImages: dummyAttachedImages, error: nil)
65 |
66 | // When
67 | self.store.execute(.mutateAttachImage(response: dummyResponse))
68 |
69 | // Then
70 | XCTAssertEqual(self.state.domainState.attachedImages, dummyAttachedImages)
71 | XCTAssertEqual(self.state.attachedImages, dummyAttachedImages.map({ $0.uiData }))
72 | }
73 |
74 | func test_mutateUploadImage__error_emptyAttachedImage() {
75 | // Given
76 | let dummyResponse = UploadReceipt.UploadImage.Response(uploadUrlObjectKeys: [], error: .emptyAttachedImage)
77 |
78 | // When
79 | self.store.execute(.mutateUploadImage(response: dummyResponse))
80 |
81 | // Then
82 | XCTAssertEqual(self.mockWorker.showToastMessage, "견적 요청을 하기 위해서\n필수로 이미지를 첨부하여야 합니다")
83 | }
84 |
85 | func test_mutateUploadImage__error_default() {
86 | // Given
87 | let dummyResponse = UploadReceipt.UploadImage.Response(uploadUrlObjectKeys: [], error: .default(NSError(domain: "Test_Error", code: -999, userInfo: nil)))
88 |
89 | // When
90 | self.store.execute(.mutateUploadImage(response: dummyResponse))
91 |
92 | // Then
93 | XCTAssertEqual(self.mockWorker.showToastMessage, "작업을 완료할 수 없습니다.(Test_Error 오류 -999.)")
94 | }
95 |
96 | func test_mutateUploadImage__success() async {
97 | // Given
98 | let dummyUploadUrlObjectKeys = ["test_uploadUrlObjectKey"]
99 | let dummyResponse = UploadReceipt.UploadImage.Response(uploadUrlObjectKeys: dummyUploadUrlObjectKeys, error: nil)
100 |
101 | // When
102 | self.store.execute(.mutateUploadImage(response: dummyResponse))
103 |
104 | // Then
105 | XCTAssertEqual(self.state.receiptImageUploadUrlObjectKeys, dummyUploadUrlObjectKeys)
106 | XCTAssertTrue(self.state.isAddRequestsViewActive)
107 | }
108 |
109 | func test_mutateSaveImage__error() async {
110 | // Given
111 | let dummyResponse = UploadReceipt.SaveImage.Response(error: NSError(domain: "Test_Error", code: -999, userInfo: nil))
112 |
113 | // When
114 | self.store.execute(.mutateSaveImage(response: dummyResponse))
115 |
116 | // Then
117 | XCTAssertEqual(self.mockWorker.showToastMessage, "작업을 완료할 수 없습니다.(Test_Error 오류 -999.)")
118 | }
119 |
120 | func test_mutateSaveImage__success() async {
121 | // Given
122 | let dummyResponse = UploadReceipt.SaveImage.Response(error: nil)
123 |
124 | // When
125 | self.store.execute(.mutateSaveImage(response: dummyResponse))
126 |
127 | // Then
128 | XCTAssertNil(self.mockWorker.showToastMessage)
129 | }
130 |
131 | func test_mutateShowCamera__permissionDenied_true() async {
132 | // Given
133 | let dummyResponse = UploadReceipt.ShowCamera.Response(permissionDenied: true)
134 |
135 | // When
136 | self.store.execute(.mutateShowCamera(response: dummyResponse))
137 |
138 | // Then
139 | XCTAssertEqual(self.mockWorker.showToastMessage, "명세표를 업로드하기 위해서\n카메라 및 모든 사진 접근 권한이 필요합니다")
140 | XCTAssertFalse(self.state.showingCamera)
141 | }
142 |
143 | func test_mutateShowCamera__permissionDenied_false() async {
144 | // Given
145 | let dummyResponse = UploadReceipt.ShowCamera.Response(permissionDenied: false)
146 |
147 | // When
148 | self.store.execute(.mutateShowCamera(response: dummyResponse))
149 |
150 | // Then
151 | XCTAssertNil(self.mockWorker.showToastMessage)
152 | XCTAssertTrue(self.state.showingCamera)
153 | }
154 |
155 | func test_mutateShowGallery__permissionDenied_true() async {
156 | // Given
157 | let dummyResponse = UploadReceipt.ShowGallery.Response(permissionDenied: true)
158 |
159 | // When
160 | self.store.execute(.mutateShowGallery(response: dummyResponse))
161 |
162 | // Then
163 | XCTAssertEqual(self.mockWorker.showToastMessage, "명세표를 업로드하기 위해서\n 모든 사진 접근 권한이 필요합니다")
164 | XCTAssertFalse(self.state.showingImagePicker)
165 | }
166 |
167 | func test_mutateShowGallery__permissionDenied_false() async {
168 | // Given
169 | let dummyResponse = UploadReceipt.ShowGallery.Response(permissionDenied: false)
170 |
171 | // When
172 | self.store.execute(.mutateShowGallery(response: dummyResponse))
173 |
174 | // Then
175 | XCTAssertNil(self.mockWorker.showToastMessage)
176 | XCTAssertTrue(self.state.showingImagePicker)
177 | }
178 |
179 | func test_showSelectImageAttachmentMethodSheet() async {
180 | // Given
181 |
182 | // When
183 | self.store.execute(.showSelectImageAttachmentMethodSheet)
184 |
185 | // Then
186 | XCTAssertTrue(self.state.showingSelectImageAttachmentMethodSheet)
187 | }
188 |
189 | func test_dismissSelectImageAttachmentMethodSheet() async {
190 | // Given
191 |
192 | // When
193 | self.store.execute(.dismissSelectImageAttachmentMethodSheet)
194 |
195 | // Then
196 | XCTAssertFalse(self.state.showingSelectImageAttachmentMethodSheet)
197 | }
198 |
199 | func test_dismissCamera() async {
200 | // Given
201 |
202 | // When
203 | self.store.execute(.dismissCamera)
204 |
205 | // Then
206 | XCTAssertFalse(self.state.showingCamera)
207 | }
208 |
209 | func test_dismissImagePicker() async {
210 | // Given
211 |
212 | // When
213 | self.store.execute(.dismissImagePicker)
214 |
215 | // Then
216 | XCTAssertFalse(self.state.showingImagePicker)
217 | }
218 |
219 | func test_showMessage() async {
220 | // Given
221 | let dummyMessage = "Test"
222 |
223 | // When
224 | self.store.execute(.showMessage(message: dummyMessage))
225 |
226 | // Then
227 | XCTAssertEqual(self.mockWorker.showToastMessage, dummyMessage)
228 | }
229 |
230 | func test_clearAttachedImages() async {
231 | // Given
232 |
233 | // When
234 | self.store.execute(.clearAttachedImages)
235 |
236 | // Then
237 | XCTAssertEqual(self.state.attachedImages, [])
238 | XCTAssertEqual(self.state.domainState.attachedImages, [])
239 | }
240 |
241 | func test_setIsActiveAddRequestsView() async {
242 | // Given
243 | let dummyIsActive = true
244 |
245 | // When
246 | self.store.execute(.setIsActiveAddRequestsView(isActive: dummyIsActive))
247 |
248 | // Then
249 | XCTAssertEqual(self.state.isAddRequestsViewActive, dummyIsActive)
250 | }
251 | }
252 |
253 | // MARK: - Mock Classes
254 |
255 | extension UploadReceiptStoreTests {
256 |
257 | class MockWorker: UploadReceiptWorkable {
258 | var delegate: UploadReceiptDelegate?
259 | var addRequestsController: AddRequestsControllerable?
260 |
261 | var showToastMessage: String?
262 |
263 | @MainActor func showToast(message: String) {
264 | self.showToastMessage = message
265 | }
266 |
267 | func mapToData(from imageUIData: UploadReceipt.ImageUIDataModel) -> Data? {
268 | return nil
269 | }
270 |
271 | func fetchImageUploadUrls(names: [String]) async throws -> [UploadReceipt.ImageUploadUrl] {
272 | return []
273 | }
274 |
275 | func requestImagesUpload(attatchedImages: [UploadReceipt.ImageModel], imageUploadUrls: [UploadReceipt.ImageUploadUrl]) async throws {
276 | return
277 | }
278 |
279 | func requestCameraPermission() async -> Bool {
280 | return false
281 | }
282 |
283 | func requestGalleryPermission() async -> Bool {
284 | return false
285 | }
286 |
287 | func saveImage(imageUIData: UploadReceipt.ImageUIDataModel) async throws {
288 | return
289 | }
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/Example/Example/Scene/UploadReceiptScene/Sources/UploadReceiptScene/View/Any/UploadReceiptUIKitView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadReceiptView.swift
3 | // Example
4 | //
5 | // Created by 박건우 on 2024/01/12.
6 | // Copyright (c) 2024 Spoqa. All rights reserved.
7 | //
8 |
9 | import AddRequestsScene
10 | import NetworkService
11 |
12 | import Combine
13 | import UIKit
14 |
15 | public final class UploadReceiptUIKitView: UIViewController {
16 | private var cancellables: Set = []
17 |
18 | private let controller: (UploadReceiptControllerable & AddRequestsDelegate)
19 | private var store: UploadReceiptStore
20 |
21 | public init(
22 | initialState: UploadReceiptState,
23 | controller: inout UploadReceiptControllerable?,
24 | delegate: UploadReceiptDelegate
25 | ) {
26 | let imageUploadNetworkService = ImageUploadNetworkService()
27 | let worker = UploadReceiptWorker(delegate: delegate, imageUploadNetworkService: imageUploadNetworkService)
28 | let store = UploadReceiptStore(worker: worker, state: initialState)
29 | let interactor = UploadReceiptInteractor(store: store, worker: worker)
30 | let _controller = UploadReceiptController(interactor: interactor, store: store)
31 | controller = _controller
32 |
33 | self.controller = _controller
34 | self.store = store
35 |
36 | super.init(nibName: nil, bundle: nil)
37 |
38 | self.bind()
39 | }
40 |
41 | required init?(coder aDecoder: NSCoder) {
42 | fatalError("init(coder:) has not been implemented")
43 | }
44 |
45 | // MARK: - UI
46 |
47 | private var titleLabel = UILabel()
48 | private var imageScrollView = UIScrollView()
49 | private var imageAttachButton = UIButton()
50 | private var nextButton = UIButton()
51 | private var imageViews = [UIImageView]()
52 | private var selectImageAttachmentMethodSheet = UIAlertController(title: "사진 추가하기", message: nil, preferredStyle: .actionSheet)
53 | private var imagePicker = UIImagePickerController()
54 | private var camera = UIImagePickerController()
55 |
56 | // MARK: - View lifecycle
57 |
58 | public override func viewDidLoad() {
59 | super.viewDidLoad()
60 |
61 | self.setupUI()
62 | }
63 |
64 | // MARK: - Layout
65 |
66 | private func setupUI() {
67 | self.view.backgroundColor = .white
68 |
69 | // Navigation Setup
70 | self.title = "견적요청 - 명세표 업로드"
71 |
72 | // ScrollView Setup
73 | self.imageScrollView.translatesAutoresizingMaskIntoConstraints = false
74 | self.imageScrollView.showsHorizontalScrollIndicator = false
75 | self.view.addSubview(self.imageScrollView)
76 |
77 | // Attach Image Button Setup
78 | self.imageAttachButton.translatesAutoresizingMaskIntoConstraints = false
79 | self.imageAttachButton.setTitle("사진 첨부", for: .normal)
80 | self.imageAttachButton.setTitleColor(.blue, for: .normal)
81 | self.imageAttachButton.addTarget(self, action: #selector(imageAttachButtonTapped), for: .touchUpInside)
82 | self.imageAttachButton.translatesAutoresizingMaskIntoConstraints = false
83 | self.view.addSubview(self.imageAttachButton)
84 |
85 | // Next Button Setup
86 | self.nextButton.translatesAutoresizingMaskIntoConstraints = false
87 | self.nextButton.setTitle("다음", for: .normal)
88 | self.nextButton.backgroundColor = .blue
89 | self.nextButton.layer.cornerRadius = 10
90 | self.nextButton.addTarget(self, action: #selector(self.nextButtonTapped), for: .touchUpInside)
91 | self.nextButton.translatesAutoresizingMaskIntoConstraints = false
92 | self.view.addSubview(self.nextButton)
93 |
94 | // Action Sheet Setup
95 | self.selectImageAttachmentMethodSheet.addAction(UIAlertAction(title: "사진 촬영하기", style: .default) { _ in
96 | self.controller.execute(.imageAttachmentMethodCameraSelected)
97 | })
98 | self.selectImageAttachmentMethodSheet.addAction(UIAlertAction(title: "사진첩에서 가져오기", style: .default) { _ in
99 | self.controller.execute(.imageAttachmentMethodGallerySelected)
100 | })
101 | self.selectImageAttachmentMethodSheet.addAction(UIAlertAction(title: "취소", style: .cancel))
102 |
103 | // ImagePicker & Camera Setup
104 | self.imagePicker.delegate = self
105 | self.imagePicker.sourceType = .photoLibrary
106 | self.camera.delegate = self
107 | self.camera.sourceType = .camera
108 |
109 | // Auto Layout Constraints
110 | NSLayoutConstraint.activate([
111 | self.imageScrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 20),
112 | self.imageScrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
113 | self.imageScrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
114 | self.imageScrollView.heightAnchor.constraint(equalToConstant: 100),
115 |
116 | self.imageAttachButton.topAnchor.constraint(equalTo: self.imageScrollView.bottomAnchor, constant: 10),
117 | self.imageAttachButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
118 | self.imageAttachButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
119 | self.imageAttachButton.heightAnchor.constraint(equalToConstant: 50),
120 |
121 | self.nextButton.topAnchor.constraint(equalTo: self.imageAttachButton.bottomAnchor, constant: 10),
122 | self.nextButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10),
123 | self.nextButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
124 | self.nextButton.heightAnchor.constraint(equalToConstant: 50)
125 | ])
126 | }
127 |
128 | // MARK: - Bind
129 |
130 | private func bind() {
131 |
132 | self.store.$state
133 | .map { $0.attachedImages }
134 | .sink { [weak self] attachedImages in
135 | self?.updateImageScrollView(with: attachedImages)
136 | }
137 | .store(in: &self.cancellables)
138 |
139 | self.store.$state
140 | .map { $0.showingSelectImageAttachmentMethodSheet }
141 | .removeDuplicates()
142 | .sink { [weak self] showingSelectImageAttachmentMethodSheet in
143 | guard let self = self else {
144 | return
145 | }
146 | if showingSelectImageAttachmentMethodSheet {
147 | self.present(self.selectImageAttachmentMethodSheet, animated: true)
148 | }
149 | else {
150 | self.selectImageAttachmentMethodSheet.dismiss(animated: true)
151 | }
152 | }
153 | .store(in: &self.cancellables)
154 |
155 | self.store.$state
156 | .map { $0.showingImagePicker }
157 | .removeDuplicates()
158 | .sink { [weak self] showingImagePicker in
159 | guard let self = self else {
160 | return
161 | }
162 | if showingImagePicker {
163 | self.present(self.imagePicker, animated: true, completion: nil)
164 | }
165 | else {
166 | self.imagePicker.dismiss(animated: true)
167 | }
168 | }
169 | .store(in: &self.cancellables)
170 |
171 | self.store.$state
172 | .map { $0.showingCamera }
173 | .removeDuplicates()
174 | .sink { [weak self] showingCamera in
175 | guard let self = self else {
176 | return
177 | }
178 | if showingCamera {
179 | self.present(self.camera, animated: true, completion: nil)
180 | }
181 | else {
182 | self.camera.dismiss(animated: true)
183 | }
184 | }
185 | .store(in: &self.cancellables)
186 |
187 | // MARK: - Bind AddReuqests
188 |
189 | self.store.$state
190 | .map { $0.isAddRequestsViewActive }
191 | .removeDuplicates()
192 | .sink { [weak self] isActive in
193 | guard let self = self else {
194 | return
195 | }
196 | if isActive {
197 | let addRequestsView = AddRequestsUIKitView(
198 | initialState: AddRequestsState(
199 | receiptImageUploadUrlObjectKeys: self.store.state.receiptImageUploadUrlObjectKeys
200 | ),
201 | controller: &self.store.worker.addRequestsController,
202 | delegate: self.controller
203 | ) { [weak self] isActive in
204 | self?.controller.execute(.addRequestsViewIsActiveChanged(isActive: isActive))
205 | }
206 | self.navigationController?.pushViewController(addRequestsView, animated: true)
207 | }
208 | else {
209 | self.navigationController?.removeViewControllersAbove(baseViewController: self, ofType: AddRequestsUIKitView.self)
210 | }
211 | }
212 | .store(in: &self.cancellables)
213 | }
214 |
215 | private func updateImageScrollView(with images: [UIImage]) {
216 | self.imageViews.forEach {
217 | $0.removeFromSuperview()
218 | }
219 | self.imageViews = images.map { image in
220 | let imageView = UIImageView(image: image)
221 | imageView.translatesAutoresizingMaskIntoConstraints = false
222 | imageView.contentMode = .scaleAspectFill
223 | imageView.clipsToBounds = true
224 | return imageView
225 | }
226 | let stackView = UIStackView(arrangedSubviews: imageViews)
227 | stackView.translatesAutoresizingMaskIntoConstraints = false
228 | stackView.axis = .horizontal
229 | stackView.spacing = 10
230 | stackView.alignment = .center
231 |
232 | self.imageScrollView.addSubview(stackView)
233 |
234 | NSLayoutConstraint.activate([
235 | stackView.topAnchor.constraint(equalTo: self.imageScrollView.topAnchor),
236 | stackView.bottomAnchor.constraint(equalTo: self.imageScrollView.bottomAnchor),
237 | stackView.leadingAnchor.constraint(equalTo: self.imageScrollView.leadingAnchor),
238 | stackView.trailingAnchor.constraint(equalTo: self.imageScrollView.trailingAnchor),
239 | stackView.heightAnchor.constraint(equalTo: self.imageScrollView.heightAnchor)
240 | ])
241 |
242 | self.imageViews.forEach { imageView in
243 | NSLayoutConstraint.activate([
244 | imageView.widthAnchor.constraint(equalToConstant: 100),
245 | imageView.heightAnchor.constraint(equalToConstant: 100)
246 | ])
247 | }
248 | }
249 |
250 | // MARK: - Action Selector
251 |
252 | @objc private func imageAttachButtonTapped() {
253 | self.controller.execute(.imageAttachTapped)
254 | }
255 |
256 | @objc private func nextButtonTapped() {
257 | self.controller.execute(.nextButtonTapped)
258 | }
259 | }
260 |
261 | extension UploadReceiptUIKitView: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
262 |
263 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
264 | guard let image = info[.originalImage] as? UIImage else {
265 | return
266 | }
267 | switch picker.sourceType {
268 | case .camera:
269 | self.controller.execute(.cameraPhotoTakenCompleted(image: image))
270 |
271 | case .photoLibrary:
272 | self.controller.execute(.imagePicked(image: image))
273 |
274 | default:
275 | break
276 | }
277 | }
278 |
279 | public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
280 | switch picker.sourceType {
281 | case .camera:
282 | self.controller.execute(.cameraCanceled)
283 |
284 | case .photoLibrary:
285 | self.controller.execute(.imagePickerCanceled)
286 |
287 | default:
288 | break
289 | }
290 | }
291 | }
292 |
293 | // MARK: - Utils
294 |
295 | extension UINavigationController {
296 |
297 | func removeViewControllersAbove(baseViewController: UIViewController, ofType viewControllerTypeToRemove: T.Type) {
298 | var viewControllers = self.viewControllers
299 |
300 | if let baseIndex = viewControllers.firstIndex(of: baseViewController) {
301 | var controllersToKeep = Array(viewControllers.prefix(through: baseIndex))
302 |
303 | for controller in viewControllers.suffix(from: baseIndex + 1) {
304 | if !(controller is T) {
305 | controllersToKeep.append(controller)
306 | }
307 | }
308 |
309 | viewControllers = controllersToKeep
310 | }
311 |
312 | self.setViewControllers(viewControllers, animated: true)
313 | }
314 | }
315 |
--------------------------------------------------------------------------------