├── 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 | [![Language](https://img.shields.io/badge/swift-5.8-orange.svg)](https://swift.org) 5 | [![Platform](https://img.shields.io/badge/iOS-13-green.svg)](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 | --------------------------------------------------------------------------------