├── LayerPackage ├── README.md ├── Tests │ ├── PresentationLayerTests │ │ ├── XCTestManifests.swift │ │ ├── FakeServiceRepository.swift │ │ └── ViewModelTests │ │ │ └── TokenViewModelTests.swift │ ├── MainTests.swift │ ├── DomainLayerTests │ │ ├── XCTestManifests.swift │ │ ├── EntityTests │ │ │ └── ServiceModelTests.swift │ │ ├── UseCaseTests │ │ │ ├── ModifyServiceTests.swift │ │ │ ├── FetchServiceTests.swift │ │ │ └── InsertServiceTests.swift │ │ └── FakeServiceRepository.swift │ └── DataLayerTests │ │ ├── XCTestManifests.swift │ │ ├── DataSourceTests │ │ └── ServiceMockDataSourceTests.swift │ │ └── RepositoryTests │ │ └── ServiceRepositoryTests.swift ├── Sources │ ├── DomainLayer │ │ ├── Entity │ │ │ ├── ServiceError.swift │ │ │ └── ServiceModel.swift │ │ ├── Interface │ │ │ ├── ServiceUseCase.swift │ │ │ └── ServiceRepositoryInterface.swift │ │ └── UseCase │ │ │ ├── ModifyServiceUseCase.swift │ │ │ ├── FetchServiceListUseCase.swift │ │ │ └── InsertServiceUseCase.swift │ ├── PresentationLayer │ │ ├── Dependency │ │ │ └── AppDIInterface.swift │ │ ├── State │ │ │ ├── Settings.swift │ │ │ └── AppState.swift │ │ ├── View │ │ │ ├── TokenDetailView.swift │ │ │ └── TokenView.swift │ │ └── ViewModel │ │ │ └── TokenViewModel.swift │ └── DataLayer │ │ ├── Extension │ │ └── Publisher+Concurrency.swift │ │ ├── Repository │ │ └── ServiceRepository.swift │ │ └── DataSource │ │ └── ServiceMockDataSource.swift └── Package.swift ├── HelloCleanArchitectureWithSwiftUI ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── HelloCleanArchitectureWithSwiftUIApp.swift └── DependecyInjection │ └── AppDI.swift ├── HelloCleanArchitectureWithSwiftUI.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── mindw.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── mindw.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── HelloCleanArchitectureWithSwiftUI.xcscheme └── project.pbxproj ├── HelloCleanArchitectureWithSwiftUITests └── HelloCleanArchitectureWithSwiftUITests.swift ├── HelloCleanArchitectureWithSwiftUIUITests └── HelloCleanArchitectureWithSwiftUIUITests.swift ├── .gitignore └── README.md /LayerPackage/README.md: -------------------------------------------------------------------------------- 1 | # LayerPackage 2 | 3 | 4 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/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 | -------------------------------------------------------------------------------- /LayerPackage/Tests/PresentationLayerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TokenViewModelTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/Entity/ServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceError.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum ServiceError: Error { 11 | case notExist 12 | case unknown 13 | } 14 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/project.xcworkspace/xcuserdata/mindw.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mind0w/HelloCleanArchitectureWithSwiftUI/HEAD/HelloCleanArchitectureWithSwiftUI.xcodeproj/project.xcworkspace/xcuserdata/mindw.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LayerPackage/Tests/MainTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DomainLayerTests 4 | import DataLayerTests 5 | import PresentationLayerTests 6 | 7 | var tests = [XCTestCaseEntry]() 8 | tests += DomainLayerTests.allTests() 9 | tests += DataLayerTests.allTests() 10 | tests += PresentationLayerTests.allTests() 11 | XCTMain(tests) 12 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ServiceModelTests.allTests), 7 | testCase(FetchServiceTests.allTests), 8 | testCase(InsertServiceTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DataLayerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ServiceDTOTests.allTests), 7 | testCase(ServiceMockDataSourceTests.allTests), 8 | testCase(ServiceRepositoryTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/Dependency/AppDIInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDIInterface.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import DomainLayer 10 | 11 | public protocol AppDIInterface { 12 | var tokenViewModel: TokenViewModel { get } 13 | } 14 | 15 | public class MockDI: AppDIInterface { 16 | public var tokenViewModel = TokenViewModel() 17 | 18 | public init() { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/Interface/ServiceUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceUseCase.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol ServiceUseCase { 12 | associatedtype RequestValue 13 | associatedtype ResponseValue 14 | var repository: ServiceRepositoryInterface { get } 15 | 16 | init(repository: ServiceRepositoryInterface) 17 | func execute(value: RequestValue) -> ResponseValue 18 | } 19 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/Interface/ServiceRepositoryInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceRepositoryInterface.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public protocol ServiceRepositoryInterface { 12 | func insertService(value: InsertServiceRequestValue) -> AnyPublisher 13 | func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> 14 | func modifyService(_ service: ServiceModel) -> AnyPublisher 15 | } 16 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/HelloCleanArchitectureWithSwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelloCleanArchitectureWithSwiftUIApp.swift 3 | // HelloCleanArchitectureWithSwiftUI 4 | // 5 | // Created by mindw on 2022/02/22. 6 | // 7 | 8 | import SwiftUI 9 | import PresentationLayer 10 | 11 | @main 12 | struct HelloCleanArchitectureWithSwiftUIApp: App { 13 | let appState = AppState(di: AppDI.shared) 14 | var body: some Scene { 15 | WindowGroup { 16 | TokenView(viewModel: AppDI.shared.tokenViewModel) 17 | .environmentObject(appState) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/State/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // 4 | // 5 | // Created by mindw on 2022/05/19. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | private enum AppStorageKey: String { 12 | /// 테마 13 | case theme 14 | } 15 | 16 | public class Settings: ObservableObject { 17 | 18 | @AppStorage(AppStorageKey.theme.rawValue) 19 | public var theme = ThemeType.light 20 | 21 | public init() {} 22 | } 23 | 24 | public enum ThemeType: String, CaseIterable { 25 | case auto = "Auto System setting" 26 | case light = "Light" 27 | case dark = "Dark" 28 | } 29 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/UseCase/ModifyServiceUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifyServiceUseCase.swift 3 | // 4 | // 5 | // Created by mindw on 2022/05/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | public struct ModifyServiceUseCase: ServiceUseCase { 13 | typealias RequestValue = ServiceModel 14 | typealias ResponseValue = AnyPublisher 15 | let repository: ServiceRepositoryInterface 16 | 17 | public init(repository: ServiceRepositoryInterface) { 18 | self.repository = repository 19 | } 20 | 21 | public func execute(value: ServiceModel) -> AnyPublisher { 22 | return repository.modifyService(value) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/UseCase/FetchServiceListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchServiceListUseCase.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | public struct FetchServiceListUseCase: ServiceUseCase { 13 | typealias RequestValue = Void 14 | typealias ResponseValue = AnyPublisher<[ServiceModel], Never> 15 | let repository: ServiceRepositoryInterface 16 | 17 | public init(repository: ServiceRepositoryInterface) { 18 | self.repository = repository 19 | } 20 | 21 | public func execute(value: Void) -> AnyPublisher<[ServiceModel], Never> { 22 | return repository.fetchServiceList() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/State/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // 4 | // 5 | // Created by mindw on 2022/05/19. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public class AppState: ObservableObject { 12 | let di: AppDIInterface 13 | @Published var settings: Settings 14 | 15 | var settingCancellable: AnyCancellable? 16 | 17 | public init(di: AppDIInterface = MockDI(), 18 | settings: Settings = Settings()) { 19 | self.di = di 20 | self.settings = settings 21 | 22 | settingCancellable = settings.objectWillChange 23 | .receive(on: DispatchQueue.main) 24 | .sink(receiveValue: { [weak self] _ in 25 | self?.objectWillChange.send() 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/Entity/ServiceModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceModel.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ServiceModel: Identifiable { 11 | public var id: Int64 = 0 12 | public var otpCode: String? 13 | public var serviceName: String? 14 | public var additionalInfo: String? 15 | public var period: Int 16 | 17 | public init(id: Int64 = 0, 18 | otpCode: String? = nil, 19 | serviceName: String? = nil, 20 | additionalInfo: String? = nil, 21 | period: Int = 30) { 22 | self.id = id 23 | self.otpCode = otpCode 24 | self.serviceName = serviceName 25 | self.additionalInfo = additionalInfo 26 | self.period = period 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/xcuserdata/mindw.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | HelloCleanArchitectureWithSwiftUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | D9BA919027C470E40041FBCB 16 | 17 | primary 18 | 19 | 20 | D9BA91A027C470E60041FBCB 21 | 22 | primary 23 | 24 | 25 | D9BA91AA27C470E60041FBCB 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/EntityTests/ServiceModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherEntityTests.swift 3 | // 4 | // 5 | // Created by mindw on 2021/02/12. 6 | // 7 | 8 | import XCTest 9 | @testable import DomainLayer 10 | 11 | final class ServiceModelTests: XCTestCase { 12 | 13 | //MARK: - Setup 14 | 15 | override func setUpWithError() throws { 16 | } 17 | 18 | //MARK: - Tests 19 | 20 | func testExecute() { 21 | 22 | let serviceModel = ServiceModel(id: 123, 23 | otpCode: "123 123", 24 | serviceName: "Google", 25 | additionalInfo: "test@google.com") 26 | 27 | XCTAssertNotNil(serviceModel) 28 | XCTAssertEqual(serviceModel.id, 123) 29 | } 30 | 31 | static var allTests = [ 32 | ("testExecute", testExecute), 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DataLayer/Extension/Publisher+Concurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Concurrency.swift 3 | // 4 | // 5 | // Created by mindw on 2022/02/22. 6 | // 7 | 8 | import Combine 9 | 10 | extension Publisher { 11 | func asyncMap( 12 | _ transform: @escaping (Output) async -> T 13 | ) -> Publishers.FlatMap, Self> { 14 | flatMap { value in 15 | Future { promise in 16 | Task { 17 | let output = await transform(value) 18 | promise(.success(output)) 19 | } 20 | } 21 | } 22 | } 23 | 24 | func tryAsyncMap( 25 | _ transform: @escaping (Output) async throws -> T 26 | ) -> Publishers.FlatMap, Self> { 27 | flatMap { value in 28 | Future { promise in 29 | Task { 30 | do { 31 | let output = try await transform(value) 32 | promise(.success(output)) 33 | } catch { 34 | promise(.failure(error)) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DomainLayer/UseCase/InsertServiceUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsertServiceUseCase.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public struct InsertServiceRequestValue { 12 | public let serviceName: String? 13 | public let secretKey: String? 14 | public let additionalInfo: String? 15 | 16 | public init(serviceName: String? = nil, 17 | secretKey: String? = nil, 18 | additionalInfo: String? = nil) { 19 | self.serviceName = serviceName 20 | self.secretKey = secretKey 21 | self.additionalInfo = additionalInfo 22 | } 23 | } 24 | 25 | public struct InsertServiceUseCase: ServiceUseCase { 26 | typealias RequestValue = InsertServiceRequestValue 27 | typealias ResponseValue = AnyPublisher 28 | let repository: ServiceRepositoryInterface 29 | 30 | public init(repository: ServiceRepositoryInterface) { 31 | self.repository = repository 32 | } 33 | 34 | public func execute(value: InsertServiceRequestValue) -> AnyPublisher { 35 | return repository.insertService(value: value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/UseCaseTests/ModifyServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifyServiceTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/05/20. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import DomainLayer 11 | 12 | final class ModifyServiceTests: XCTestCase { 13 | 14 | var useCase: ModifyServiceUseCase? 15 | private var bag: Set = Set() 16 | 17 | //MARK: - Setup 18 | 19 | override func setUpWithError() throws { 20 | 21 | self.useCase = ModifyServiceUseCase(repository: FakeServiceRepository()) 22 | 23 | guard self.useCase != nil else { 24 | XCTFail("Usecase is nil") 25 | return 26 | } 27 | } 28 | 29 | //MARK: - Tests 30 | func testExecute() { 31 | 32 | useCase?.execute(value: .init()) 33 | .sink { receiveCompletion in 34 | switch receiveCompletion { 35 | case .failure(let error): 36 | XCTFail(error.localizedDescription) 37 | case .finished: 38 | break 39 | } 40 | } receiveValue: { success in 41 | XCTAssertTrue(success) 42 | } 43 | .store(in: &bag) 44 | } 45 | 46 | static var allTests = [ 47 | ("testExecute", testExecute), 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/FakeServiceRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeServiceRepository.swift 3 | // Tests iOS 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import DomainLayer 11 | 12 | struct FakeServiceRepository: ServiceRepositoryInterface { 13 | func insertService(value: InsertServiceRequestValue) -> AnyPublisher { 14 | return Just(ServiceModel(id: 0, 15 | otpCode: value.secretKey, 16 | serviceName: value.serviceName, 17 | additionalInfo: value.additionalInfo)) 18 | .setFailureType(to: Error.self) 19 | .eraseToAnyPublisher() 20 | } 21 | 22 | func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> { 23 | 24 | let mockedList = [ 25 | ServiceModel(id: 0, serviceName: "0"), 26 | ServiceModel(id: 1, serviceName: "1"), 27 | ServiceModel(id: 2, serviceName: "2"), 28 | ServiceModel(id: 3, serviceName: "3"), 29 | ServiceModel(id: 4, serviceName: "4") 30 | ] 31 | 32 | return Just(mockedList) 33 | .eraseToAnyPublisher() 34 | } 35 | 36 | func modifyService(_ service: ServiceModel) -> AnyPublisher { 37 | return Just(true).eraseToAnyPublisher() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LayerPackage/Tests/PresentationLayerTests/FakeServiceRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeServiceRepository.swift 3 | // Tests iOS 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import DomainLayer 11 | 12 | struct FakeServiceRepository: ServiceRepositoryInterface { 13 | func insertService(value: InsertServiceRequestValue) -> AnyPublisher { 14 | return Just(ServiceModel(id: 0, 15 | otpCode: value.secretKey, 16 | serviceName: value.serviceName, 17 | additionalInfo: value.additionalInfo)) 18 | .setFailureType(to: Error.self) 19 | .eraseToAnyPublisher() 20 | } 21 | 22 | func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> { 23 | 24 | let mockedList = [ 25 | ServiceModel(id: 0, serviceName: "0"), 26 | ServiceModel(id: 1, serviceName: "1"), 27 | ServiceModel(id: 2, serviceName: "2"), 28 | ServiceModel(id: 3, serviceName: "3"), 29 | ServiceModel(id: 4, serviceName: "4") 30 | ] 31 | 32 | return Just(mockedList) 33 | .eraseToAnyPublisher() 34 | } 35 | 36 | func modifyService(_ service: ServiceModel) -> AnyPublisher { 37 | return Just(true).eraseToAnyPublisher() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/DependecyInjection/AppDI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDI.swift 3 | // 4 | // Created by mindw on 2022/01/03. 5 | // 6 | 7 | import Foundation 8 | import DataLayer 9 | import DomainLayer 10 | import PresentationLayer 11 | 12 | enum PHASE { 13 | case DEV, ALPHA, REAL 14 | } 15 | 16 | public struct AppEnvironment { 17 | let phase: PHASE = .DEV 18 | } 19 | 20 | public class AppDI: AppDIInterface { 21 | 22 | static let shared = AppDI(appEnvironment: AppEnvironment()) 23 | 24 | private let appEnvironment: AppEnvironment 25 | 26 | private init(appEnvironment: AppEnvironment) { 27 | self.appEnvironment = appEnvironment 28 | } 29 | 30 | private lazy var serviceRepository: ServiceRepositoryInterface = { 31 | let dataSource: ServiceDataSourceInterface 32 | 33 | switch appEnvironment.phase { 34 | case .DEV: 35 | dataSource = ServiceMockDataSource() 36 | default: 37 | dataSource = ServiceMockDataSource() 38 | } 39 | 40 | return ServiceRepository(dataSource: dataSource) 41 | }() 42 | 43 | public lazy var tokenViewModel: TokenViewModel = { 44 | return .init(fetchListUseCase: .init(repository: serviceRepository), 45 | insertServiceUseCase: .init(repository: serviceRepository), 46 | modifyServiceUseCase: .init(repository: serviceRepository)) 47 | }() 48 | } 49 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DataLayer/Repository/ServiceRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceRepository.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import DomainLayer 11 | 12 | public struct ServiceRepository: ServiceRepositoryInterface { 13 | 14 | private let dataSource: ServiceDataSourceInterface 15 | 16 | public init(dataSource: ServiceDataSourceInterface) { 17 | self.dataSource = dataSource 18 | } 19 | 20 | public func insertService(value: InsertServiceRequestValue) -> AnyPublisher { 21 | return Just(value) 22 | .setFailureType(to: Error.self) 23 | .tryAsyncMap { try await dataSource.insertService(value: $0) } 24 | .receive(on: RunLoop.main) 25 | .eraseToAnyPublisher() 26 | } 27 | 28 | public func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> { 29 | return Just(()) 30 | .asyncMap { await dataSource.fetchServiceList() } 31 | .receive(on: RunLoop.main) 32 | .eraseToAnyPublisher() 33 | } 34 | 35 | public func modifyService(_ service: ServiceModel) -> AnyPublisher { 36 | return Just(service) 37 | .asyncMap { await dataSource.modifyService($0) } 38 | .receive(on: RunLoop.main) 39 | .eraseToAnyPublisher() 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUITests/HelloCleanArchitectureWithSwiftUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelloCleanArchitectureWithSwiftUITests.swift 3 | // HelloCleanArchitectureWithSwiftUITests 4 | // 5 | // Created by mindw on 2022/02/22. 6 | // 7 | 8 | import XCTest 9 | @testable import HelloCleanArchitectureWithSwiftUI 10 | 11 | class HelloCleanArchitectureWithSwiftUITests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | // func testPerformanceExample() throws { 30 | // // This is an example of a performance test case. 31 | // self.measure { 32 | // // Put the code you want to measure the time of here. 33 | // } 34 | // } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/UseCaseTests/FetchServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchServiceTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import DomainLayer 11 | 12 | final class FetchServiceTests: XCTestCase { 13 | 14 | var useCase: FetchServiceListUseCase? 15 | private var bag: Set = Set() 16 | 17 | //MARK: - Setup 18 | 19 | override func setUpWithError() throws { 20 | 21 | self.useCase = FetchServiceListUseCase(repository: FakeServiceRepository()) 22 | 23 | guard self.useCase != nil else { 24 | XCTFail("Usecase is nil") 25 | return 26 | } 27 | } 28 | 29 | //MARK: - Tests 30 | func testExecute() { 31 | 32 | useCase?.execute(value: ()) 33 | .sink { receiveCompletion in 34 | switch receiveCompletion { 35 | case .failure(let error): 36 | XCTFail(error.localizedDescription) 37 | case .finished: 38 | break 39 | } 40 | } receiveValue: { services in 41 | XCTAssertGreaterThan(services.count, 0) 42 | for service in services { 43 | XCTAssertNotNil(service.id) 44 | } 45 | } 46 | .store(in: &bag) 47 | } 48 | 49 | static var allTests = [ 50 | ("testExecute", testExecute), 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /LayerPackage/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "LayerPackage", 8 | platforms: [.iOS(.v15), .macOS("12")], 9 | products: [ 10 | .library( 11 | name: "LayerPackage", 12 | targets: ["DataLayer", "DomainLayer", "PresentationLayer"]), 13 | 14 | ], 15 | dependencies: [ 16 | ], 17 | targets: [ 18 | 19 | //MARK: - Data Layer 20 | // Dependency Inversion : UseCase(DomainLayer) <- Repository <-> DataSource 21 | .target( 22 | name: "DataLayer", 23 | dependencies: ["DomainLayer"]), 24 | 25 | //MARK: - Domain Layer 26 | .target( 27 | name: "DomainLayer", 28 | dependencies: []), 29 | 30 | //MARK: - Presentation Layer (MVVM) 31 | // Dependency : View -> ViewModel -> Model(DomainLayer) 32 | .target( 33 | name: "PresentationLayer", 34 | dependencies: ["DomainLayer"]), 35 | 36 | //MARK: - Tests 37 | .testTarget( 38 | name: "DataLayerTests", 39 | dependencies: ["DataLayer"]), 40 | 41 | .testTarget( 42 | name: "DomainLayerTests", 43 | dependencies: ["DomainLayer"]), 44 | 45 | .testTarget( 46 | name: "PresentationLayerTests", 47 | dependencies: ["PresentationLayer"]), 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DomainLayerTests/UseCaseTests/InsertServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsertServiceTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import DomainLayer 11 | 12 | class InsertServiceTests: XCTestCase { 13 | 14 | var useCase: InsertServiceUseCase? 15 | private var bag = Set() 16 | 17 | override func setUpWithError() throws { 18 | self.useCase = InsertServiceUseCase(repository: FakeServiceRepository()) 19 | 20 | guard self.useCase != nil else { 21 | XCTFail("InsertServiceUseCase is nil") 22 | return 23 | } 24 | } 25 | 26 | func testExecute() { 27 | let req = InsertServiceRequestValue(serviceName: "Google", 28 | secretKey: "123", 29 | additionalInfo: "sample@google.com") 30 | self.useCase?.execute(value: req) 31 | .sink { completion in 32 | switch completion { 33 | case .finished: 34 | break 35 | case .failure(let err): 36 | XCTFail(err.localizedDescription) 37 | break 38 | } 39 | } receiveValue: { service in 40 | XCTAssertNotNil(service) 41 | XCTAssertNotNil(service.id) 42 | XCTAssertEqual(service.serviceName, req.serviceName) 43 | XCTAssertEqual(service.additionalInfo, req.additionalInfo) 44 | } 45 | .store(in: &bag) 46 | 47 | } 48 | 49 | static var allTests = [ 50 | ("testExecute", testExecute), 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUIUITests/HelloCleanArchitectureWithSwiftUIUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelloCleanArchitectureWithSwiftUIUITests.swift 3 | // HelloCleanArchitectureWithSwiftUIUITests 4 | // 5 | // Created by mindw on 2022/02/22. 6 | // 7 | 8 | import XCTest 9 | 10 | class HelloCleanArchitectureWithSwiftUIUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | // func testLaunchPerformance() throws { 35 | // if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // // This measures how long it takes to launch your application. 37 | // measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | // XCUIApplication().launch() 39 | // } 40 | // } 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DataLayerTests/DataSourceTests/ServiceMockDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceMockDataSourceTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import XCTest 9 | @testable import DataLayer 10 | @testable import DomainLayer 11 | 12 | class ServiceMockDataSourceTests: XCTestCase { 13 | 14 | var dataSource: ServiceMockDataSource? 15 | 16 | override func setUpWithError() throws { 17 | dataSource = ServiceMockDataSource() 18 | 19 | guard dataSource != nil else { 20 | XCTFail("DataSource is nil") 21 | return 22 | } 23 | } 24 | 25 | func testInsert() { 26 | Task { 27 | let req = InsertServiceRequestValue(serviceName: "Google", 28 | secretKey: "123", 29 | additionalInfo: "sample@google.com") 30 | let service = try await dataSource?.insertService(value: req) 31 | 32 | XCTAssertNotNil(service) 33 | XCTAssertNotNil(service!.id) 34 | XCTAssertEqual(service!.serviceName, req.serviceName) 35 | XCTAssertEqual(service!.additionalInfo, req.additionalInfo) 36 | } 37 | } 38 | 39 | func testFetchList() { 40 | Task { 41 | let services = await dataSource?.fetchServiceList() 42 | XCTAssertNotNil(services) 43 | XCTAssertGreaterThan(services!.count, 0) 44 | for service in services! { 45 | XCTAssertNotNil(service.id) 46 | XCTAssertNotNil(service.serviceName) 47 | XCTAssertNotNil(service.additionalInfo) 48 | XCTAssertNotNil(service.otpCode) 49 | } 50 | } 51 | } 52 | 53 | func testModify() { 54 | Task { 55 | let success = await dataSource?.modifyService(.init()) 56 | XCTAssertNotNil(success) 57 | XCTAssertTrue(success!) 58 | } 59 | } 60 | 61 | static var allTests = [ 62 | ("testInsert", testInsert), 63 | ("testFetchList", testFetchList), 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /LayerPackage/Sources/DataLayer/DataSource/ServiceMockDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceMockDataSource.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import DomainLayer 11 | 12 | public protocol ServiceDataSourceInterface { 13 | func insertService(value: InsertServiceRequestValue) async throws -> ServiceModel 14 | func fetchServiceList() async -> [ServiceModel] 15 | func modifyService(_ service: ServiceModel) async -> Bool 16 | } 17 | 18 | public final actor ServiceMockDataSource { 19 | // 테스트 데이터 20 | var mockDatas: [ServiceModel] = [ 21 | ServiceModel(id: 0, otpCode: "123 123", serviceName: "Google", additionalInfo: "sample@google.com"), 22 | ServiceModel(id: 1, otpCode: "456 456", serviceName: "Github", additionalInfo: "sample@github.com"), 23 | ServiceModel(id: 2, otpCode: "789 789", serviceName: "Amazon", additionalInfo: "sample@amazon.com") 24 | ] 25 | 26 | public init() {} 27 | } 28 | 29 | extension ServiceMockDataSource: ServiceDataSourceInterface { 30 | 31 | public func insertService(value: InsertServiceRequestValue) async throws -> ServiceModel { 32 | guard let serviceName = value.serviceName else { throw ServiceError.unknown } 33 | 34 | let insertData = ServiceModel(id: Int64.random(in: 0.. [ServiceModel] { 45 | return mockDatas 46 | } 47 | 48 | public func modifyService(_ service: ServiceModel) async -> Bool { 49 | if var first = mockDatas.first(where: { $0.id == service.id }) { 50 | first.otpCode = service.otpCode 51 | first.serviceName = service.serviceName 52 | first.additionalInfo = service.additionalInfo 53 | return true 54 | } 55 | 56 | return false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | Packages/ 43 | Package.pins 44 | Package.resolved 45 | # *.xcodeproj 46 | 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | #TEST 95 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/View/TokenDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenDetailView.swift 3 | // 4 | // 5 | // Created by mindw on 2022/05/20. 6 | // 7 | 8 | import SwiftUI 9 | import DomainLayer 10 | 11 | struct TokenDetailView: View { 12 | @Environment(\.presentationMode) var presentationMode 13 | @EnvironmentObject var appState: AppState 14 | 15 | @State var serviceName = "" 16 | @State var additionalInfo = "" 17 | 18 | private let viewModel: TokenViewModel 19 | private let service: ServiceModel 20 | 21 | public init(viewModel: TokenViewModel, service: ServiceModel) { 22 | self.viewModel = viewModel 23 | self.service = service 24 | _serviceName = State(initialValue: service.serviceName ?? "") 25 | _additionalInfo = State(initialValue: service.additionalInfo ?? "") 26 | } 27 | 28 | var body: some View { 29 | VStack(alignment: .center) { 30 | Text("Service name") 31 | TextField("Service name", text: $serviceName) 32 | .textFieldStyle(.roundedBorder) 33 | .padding(.bottom, 16) 34 | 35 | Text("Additional info") 36 | TextField("Additional info", text: $additionalInfo) 37 | .textFieldStyle(.roundedBorder) 38 | 39 | Spacer() 40 | } 41 | .padding() 42 | .navigationBarTitleDisplayMode(.inline) 43 | .navigationBarBackButtonHidden(true) 44 | .navigationTitle("Edit") 45 | .toolbar { 46 | ToolbarItem(placement: .cancellationAction) { 47 | Button { 48 | self.presentationMode.wrappedValue.dismiss() 49 | } label: { 50 | Image(systemName: "chevron.left") 51 | .foregroundColor(Color(UIColor.label)) 52 | } 53 | } 54 | 55 | ToolbarItem(placement: .primaryAction) { 56 | Button { 57 | self.viewModel.executeEditService(id: service.id, serviceName: serviceName, additionalInfo: additionalInfo) { 58 | self.presentationMode.wrappedValue.dismiss() 59 | } 60 | } label: { 61 | Text("Done") 62 | .foregroundColor(Color(UIColor.label)) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | struct TokenDetailView_Previews: PreviewProvider { 70 | static var previews: some View { 71 | TokenDetailView(viewModel: .init(), service: .init()) 72 | .environmentObject(AppState()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /LayerPackage/Tests/PresentationLayerTests/ViewModelTests/TokenViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenViewModelTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import DomainLayer 11 | @testable import PresentationLayer 12 | 13 | class TokenViewModelTests: XCTestCase { 14 | 15 | var viewModel: TokenViewModel? 16 | private var bag: Set = Set() 17 | 18 | override func setUpWithError() throws { 19 | let fetchServiceUseCase = FetchServiceListUseCase(repository: FakeServiceRepository()) 20 | let insertServiceUseCase = InsertServiceUseCase(repository: FakeServiceRepository()) 21 | viewModel = TokenViewModel(fetchListUseCase: fetchServiceUseCase, insertServiceUseCase: insertServiceUseCase) 22 | 23 | guard viewModel != nil else { 24 | XCTFail("ViewModel is nil") 25 | return 26 | } 27 | } 28 | 29 | func testInsert() { 30 | let expectation = XCTestExpectation(description: self.description) 31 | let serviceName = "Google" 32 | let addtionalInfo = "sample@google.com" 33 | let sercretKey = "123" 34 | 35 | viewModel?.$services 36 | .sink(receiveValue: { services in 37 | guard let service = services.last else { 38 | return 39 | } 40 | XCTAssertNotNil(service) 41 | XCTAssertNotNil(service.id) 42 | XCTAssertEqual(service.serviceName, serviceName) 43 | XCTAssertEqual(service.additionalInfo, addtionalInfo) 44 | expectation.fulfill() 45 | }) 46 | .store(in: &bag) 47 | 48 | viewModel?.executeInsertService(serviceName: serviceName, 49 | secretKey: sercretKey, 50 | additionalInfo: addtionalInfo) 51 | 52 | wait(for: [expectation], timeout: 1) 53 | } 54 | 55 | func testFetchList() { 56 | 57 | let expectation = XCTestExpectation(description: self.description) 58 | 59 | viewModel?.$services 60 | .sink(receiveValue: { services in 61 | guard services.count > 0 else { 62 | return 63 | } 64 | XCTAssertNotNil(services) 65 | for service in services { 66 | XCTAssertNotNil(service.id) 67 | XCTAssertNotNil(service.serviceName) 68 | } 69 | 70 | expectation.fulfill() 71 | }) 72 | .store(in: &bag) 73 | 74 | viewModel?.executeFetchList() 75 | 76 | wait(for: [expectation], timeout: 1) 77 | } 78 | 79 | static var allTests = [ 80 | ("testInsert", testInsert), 81 | ("testFetchList", testFetchList), 82 | ] 83 | 84 | } 85 | -------------------------------------------------------------------------------- /LayerPackage/Tests/DataLayerTests/RepositoryTests/ServiceRepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceRepositoryTests.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/10. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import DataLayer 11 | @testable import DomainLayer 12 | 13 | class ServiceRepositoryTests: XCTestCase { 14 | 15 | var repository: ServiceRepository? 16 | private var bag = Set() 17 | 18 | override func setUpWithError() throws { 19 | repository = ServiceRepository(dataSource: ServiceMockDataSource()) 20 | 21 | guard repository != nil else { 22 | XCTFail("Repository is nil") 23 | return 24 | } 25 | } 26 | 27 | func testInsert() { 28 | let req = InsertServiceRequestValue(serviceName: "Google", 29 | secretKey: "123", 30 | additionalInfo: "sample@google.com") 31 | 32 | repository?.insertService(value: req) 33 | .sink(receiveCompletion: { completion in 34 | switch completion { 35 | case .finished: 36 | break 37 | case .failure(let err): 38 | XCTFail(err.localizedDescription) 39 | break 40 | } 41 | }, receiveValue: { service in 42 | XCTAssertNotNil(service) 43 | XCTAssertNotNil(service.id) 44 | XCTAssertEqual(service.serviceName, req.serviceName) 45 | XCTAssertEqual(service.additionalInfo, req.additionalInfo) 46 | }) 47 | .store(in: &bag) 48 | } 49 | 50 | func testFetchList() { 51 | repository?.fetchServiceList() 52 | .sink(receiveCompletion: { completion in 53 | switch completion { 54 | case .finished: 55 | break 56 | case .failure(let err): 57 | XCTFail(err.localizedDescription) 58 | break 59 | } 60 | }, receiveValue: { services in 61 | XCTAssertGreaterThan(services.count, 0) 62 | for service in services { 63 | XCTAssertNotNil(service.id) 64 | XCTAssertNotNil(service.serviceName) 65 | XCTAssertNotNil(service.additionalInfo) 66 | XCTAssertNotNil(service.otpCode) 67 | } 68 | }) 69 | .store(in: &bag) 70 | } 71 | 72 | func testModify() { 73 | repository?.modifyService(.init()) 74 | .sink(receiveCompletion: { completion in 75 | switch completion { 76 | case .finished: 77 | break 78 | case .failure(let err): 79 | XCTFail(err.localizedDescription) 80 | break 81 | } 82 | }, receiveValue: { success in 83 | XCTAssertTrue(success) 84 | }) 85 | .store(in: &bag) 86 | } 87 | 88 | static var allTests = [ 89 | ("testInsert", testInsert), 90 | ("testFetchList", testFetchList), 91 | ("testModify", testModify), 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/ViewModel/TokenViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenViewModel.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import DomainLayer 11 | 12 | public protocol TokenViewModelInput { 13 | func executeFetchList() 14 | func executeInsertService(serviceName: String?, secretKey: String?, additionalInfo: String?) 15 | func executeEditService(id: Int64, serviceName: String?, additionalInfo: String?, completion: (() -> Void)?) 16 | } 17 | 18 | public protocol TokenViewModelOutput { 19 | var services: [ServiceModel] { get } 20 | } 21 | 22 | public final class TokenViewModel: ObservableObject, TokenViewModelInput, TokenViewModelOutput { 23 | @Published public var services = [ServiceModel]() 24 | 25 | private let fetchListUseCase: FetchServiceListUseCase? 26 | private let insertServiceUseCase: InsertServiceUseCase? 27 | private let modifyServiceUseCase: ModifyServiceUseCase? 28 | 29 | private var bag = Set() 30 | 31 | public init(fetchListUseCase: FetchServiceListUseCase? = nil, 32 | insertServiceUseCase: InsertServiceUseCase? = nil, 33 | modifyServiceUseCase: ModifyServiceUseCase? = nil) { 34 | self.fetchListUseCase = fetchListUseCase 35 | self.insertServiceUseCase = insertServiceUseCase 36 | self.modifyServiceUseCase = modifyServiceUseCase 37 | } 38 | 39 | public func executeFetchList() { 40 | self.fetchListUseCase?.execute(value: ()) 41 | .assign(to: \.services, on: self) 42 | .store(in: &bag) 43 | } 44 | 45 | public func executeInsertService(serviceName: String?, secretKey: String?, additionalInfo: String?) { 46 | self.insertServiceUseCase?.execute(value: InsertServiceRequestValue(serviceName: serviceName, 47 | secretKey: secretKey, 48 | additionalInfo: additionalInfo)) 49 | .sink(receiveCompletion: { completion in 50 | switch completion { 51 | case .finished: 52 | break 53 | case .failure(let error): 54 | print(error.localizedDescription) 55 | break 56 | } 57 | }, receiveValue: { service in 58 | self.services.append(service) 59 | }) 60 | .store(in: &bag) 61 | } 62 | 63 | public func executeEditService(id: Int64, 64 | serviceName: String? = nil, 65 | additionalInfo: String? = nil, 66 | completion: (() -> Void)? = nil) { 67 | var req = ServiceModel(id: id) 68 | req.serviceName = serviceName 69 | req.additionalInfo = additionalInfo 70 | self.modifyServiceUseCase?.execute(value: req) 71 | .sink(receiveValue: { success in 72 | if success, let idx = self.services.firstIndex(where: { $0.id == id }) { 73 | self.services[idx].serviceName = serviceName 74 | self.services[idx].additionalInfo = additionalInfo 75 | } 76 | completion?() 77 | }) 78 | .store(in: &bag) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /LayerPackage/Sources/PresentationLayer/View/TokenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenView.swift 3 | // 4 | // 5 | // Created by mindw on 2022/01/03. 6 | // 7 | 8 | import SwiftUI 9 | import DomainLayer 10 | 11 | public struct TokenView: View { 12 | @Environment(\.colorScheme) private var colorScheme: ColorScheme 13 | @EnvironmentObject var appState: AppState 14 | @ObservedObject var viewModel: TokenViewModel 15 | 16 | public init(viewModel: TokenViewModel) { 17 | self.viewModel = viewModel 18 | } 19 | 20 | public var body: some View { 21 | NavigationView { 22 | List { 23 | ForEach(self.viewModel.services) { service in 24 | NavigationLink { 25 | TokenDetailView(viewModel: viewModel, service: service) 26 | } label: { 27 | VStack(alignment: .leading) { 28 | Text(service.serviceName ?? "") 29 | Text(service.otpCode ?? "") 30 | .font(.title) 31 | .bold() 32 | Text(service.additionalInfo ?? "") 33 | } 34 | } 35 | .padding() 36 | } 37 | } 38 | .navigationTitle("Tokens") 39 | .toolbar { 40 | ToolbarItem(placement: .primaryAction) { 41 | HStack { 42 | Button { 43 | self.appState.settings.theme = (self.appState.settings.theme == .light) ? .dark : .light 44 | } label: { 45 | Image(systemName: "paintpalette.fill") 46 | .foregroundColor(Color(UIColor.label)) 47 | } 48 | 49 | Button { 50 | self.viewModel.executeInsertService(serviceName: "Token", 51 | secretKey: "123", 52 | additionalInfo: "insert@test.com") 53 | } label: { 54 | Image(systemName: "plus") 55 | .foregroundColor(Color(UIColor.label)) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | .onAppear { 62 | print("Token onAppear") 63 | self.viewModel.executeFetchList() 64 | } 65 | .environment(\.colorScheme, theme) 66 | } 67 | 68 | private var theme: ColorScheme { 69 | switch self.appState.settings.theme { 70 | case .auto: 71 | return self.colorScheme 72 | case .light: 73 | return .light 74 | case .dark: 75 | return .dark 76 | } 77 | } 78 | } 79 | 80 | #if DEBUG 81 | struct TokenView_Previews: PreviewProvider { 82 | static var previews: some View { 83 | let vm = TokenViewModel() 84 | vm.services.append(ServiceModel(id: 0, otpCode: "000 000", serviceName: "Name0", additionalInfo: "Info0")) 85 | vm.services.append(ServiceModel(id: 1, otpCode: "111 111", serviceName: "Name1", additionalInfo: "Info1")) 86 | return TokenView(viewModel: vm) 87 | .environmentObject(AppState()) 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/xcshareddata/xcschemes/HelloCleanArchitectureWithSwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 53 | 59 | 60 | 61 | 63 | 69 | 70 | 71 | 73 | 79 | 80 | 81 | 82 | 83 | 93 | 95 | 101 | 102 | 103 | 104 | 110 | 112 | 118 | 119 | 120 | 121 | 123 | 124 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HelloCleanArchitectureWithSwiftUI 2 | CleanArchitecture for SwiftUI with Combine, Concurrency 3 | 4 | ## 개요 5 | Clean Architecture를 SwiftUI와 Combine을 사용한 iOS 프로젝트에 적용한 예제 6 | 7 | image 8 | 9 | 10 | ## Layer와 Data Flow 11 | 먼저 역할별 레이어들부터 알아보자면 다음과 같다. 12 | * Presentation Layer: UI 관련 레이어 13 | * Domain Layer: 비즈니스 룰과 로직 담당 레이어 14 | * Data Layer: 원격/로컬등 외부에서 데이터를 가져오는 레이어 15 | 16 | ![image](https://user-images.githubusercontent.com/25020477/155071101-28765b74-9c9a-4ccb-ae19-f342288937c0.png) 17 | 18 | * 각 레이어들의 Dependency 방향은 모두 원밖에서 원안쪽으로 향하고 있음 19 | * UI를 담당하는 Presentation Layer는 MVVM 패턴으로 구현됨 20 | 21 | 22 | 각 레이어의 데이터 흐름은 다음과 같다. 23 | 24 | ![image](https://user-images.githubusercontent.com/25020477/155071125-fab08a92-4501-4bfe-916e-ac19ce549f90.png) 25 | 26 | * Domain Layer에서 Data Layer를 실행 시킬 수 있는 이유는 Dependency Inversion 으로 구현되었기 때문 27 | 28 | > Dependency Inversion 이란? 29 | > 30 | > 각 모듈간의 의존성을 분리시키기 위해 추상화된 인터페이스만 제공하고 의존성은 외부에서 주입(Dependency Injection)시킴 31 | 32 | 33 | 34 | ## 프로젝트 구성 (Swift Package Manager) 35 | Clean Architecture의 각 Layer 별 의존성을 구현하기 위해 SPM을 사용하여 프로젝트를 구성한다. 36 | 37 | * 로컬 패키지를 하나 추가하고 폴더 구조를 다음과 같이 구성한다. 38 | 39 | image 40 | 41 | * LayerPackage/Package.swift 에서 다음과 같이 Dependency를 줄 수 있음 42 | 43 | ``` swift 44 | import PackageDescription 45 | 46 | let package = Package( 47 | name: "LayerPackage", 48 | platforms: [.iOS(.v15), .macOS("12")], 49 | products: [ 50 | .library( 51 | name: "LayerPackage", 52 | targets: ["DataLayer", "DomainLayer", "PresentationLayer"]), 53 | 54 | ], 55 | dependencies: [ 56 | ], 57 | targets: [ 58 | 59 | //MARK: - Data Layer 60 | // Dependency Inversion : UseCase(DomainLayer) <- Repository <-> DataSource 61 | .target( 62 | name: "DataLayer", 63 | dependencies: ["DomainLayer"]), 64 | 65 | //MARK: - Domain Layer 66 | .target( 67 | name: "DomainLayer", 68 | dependencies: []), 69 | 70 | //MARK: - Presentation Layer (MVVM) 71 | // Dependency : View -> ViewModel -> Model(DomainLayer) 72 | .target( 73 | name: "PresentationLayer", 74 | dependencies: ["DomainLayer"]), 75 | 76 | //MARK: - Tests 77 | .testTarget( 78 | name: "DataLayerTests", 79 | dependencies: ["DataLayer"]), 80 | 81 | .testTarget( 82 | name: "DomainLayerTests", 83 | dependencies: ["DomainLayer"]), 84 | 85 | .testTarget( 86 | name: "PresentationLayerTests", 87 | dependencies: ["PresentationLayer"]), 88 | ] 89 | ) 90 | ``` 91 | 92 | 93 | ## Domain Layer 구현 94 | * 원의 가장 내부 계층이며 핵심 기능을 담당하는 데이터 구조 95 | * 상위 계층에 의존성을 갖고 있지 않음으로 독립적으로 수행 가능해야 함 96 | 97 | ``` swift 98 | public struct ServiceModel: Identifiable { 99 | public var id: Int64 = 0 100 | public var otpCode: String? 101 | public var serviceName: String? 102 | public var additinalInfo: String? 103 | public var period: Int 104 | 105 | public init(id: Int64 = 0, 106 | otpCode: String? = nil, 107 | serviceName: String? = nil, 108 | additinalInfo: String? = nil, 109 | period: Int = 30) { 110 | self.id = id 111 | self.otpCode = otpCode 112 | self.serviceName = serviceName 113 | self.additinalInfo = additinalInfo 114 | self.period = period 115 | } 116 | } 117 | ``` 118 | 119 | * Data Layer에서 구현될 Repository에 대한 인터페이스를 추상화 함으로써 Dependency Inversion 구현을 가능하도록 함 120 | 121 | ``` swift 122 | import Combine 123 | 124 | public protocol ServiceRepositoryInterface { 125 | func insertService(value: InsertServiceRequestValue) -> AnyPublisher 126 | func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> 127 | } 128 | ``` 129 | 130 | * 비즈니스 로직에 대한 각 UseCase를 구현 131 | * ServiceUseCase는 associatedtype을 활용한 UseCase 프로토콜 132 | 133 | ``` swift 134 | protocol ServiceUseCase { 135 | associatedtype RequestValue 136 | associatedtype ResponseValue 137 | var repository: ServiceRepositoryInterface { get } 138 | 139 | init(repository: ServiceRepositoryInterface) 140 | func execute(value: RequestValue) -> ResponseValue 141 | } 142 | ``` 143 | ``` swift 144 | public struct FetchServiceListUseCase: ServiceUseCase { 145 | typealias RequestValue = Void 146 | typealias ResponseValue = AnyPublisher<[ServiceModel], Never> 147 | let repository: ServiceRepositoryInterface 148 | 149 | public init(repository: ServiceRepositoryInterface) { 150 | self.repository = repository 151 | } 152 | 153 | public func execute(value: Void) -> AnyPublisher<[ServiceModel], Never> { 154 | return repository.fetchServiceList() 155 | } 156 | } 157 | ``` 158 | ``` swift 159 | public struct InsertServiceRequestValue { 160 | public let serviceName: String? 161 | public let secretKey: String? 162 | public let additionalInfo: String? 163 | 164 | public init(serviceName: String? = nil, 165 | secretKey: String? = nil, 166 | additionalInfo: String? = nil) { 167 | self.serviceName = serviceName 168 | self.secretKey = secretKey 169 | self.additionalInfo = additionalInfo 170 | } 171 | } 172 | 173 | public struct InsertServiceUseCase: ServiceUseCase { 174 | typealias RequestValue = InsertServiceRequestValue 175 | typealias ResponseValue = AnyPublisher 176 | let repository: ServiceRepositoryInterface 177 | 178 | public init(repository: ServiceRepositoryInterface) { 179 | self.repository = repository 180 | } 181 | 182 | public func execute(value: InsertServiceRequestValue) -> AnyPublisher { 183 | return repository.insertService(value: value) 184 | } 185 | } 186 | ``` 187 | 188 | ## Presentation Layer 구현 189 | * UI 를 담당하는 Layer 190 | * MVVM 패턴으로 구현 191 | * View와 ViewModel 사이는 Combine으로 Data Binding 처리 192 | 193 | ``` swift 194 | import Foundation 195 | import Combine 196 | import DomainLayer 197 | 198 | public protocol TokenViewModelInput { 199 | func executeFetchList() 200 | func executeInsertService(serviceName: String?, 201 | secretKey: String?, 202 | additionalInfo: String?) 203 | } 204 | 205 | public protocol TokenViewModelOutput { 206 | var services: [ServiceModel] { get } 207 | } 208 | 209 | public final class TokenViewModel: ObservableObject, TokenViewModelInput, TokenViewModelOutput { 210 | @Published public var services = [ServiceModel]() 211 | 212 | private let fetchListUseCase: FetchServiceListUseCase? 213 | private let insertServiceUseCase: InsertServiceUseCase? 214 | 215 | private var bag = Set() 216 | 217 | public init(fetchListUseCase: FetchServiceListUseCase? = nil, 218 | insertServiceUseCase: InsertServiceUseCase? = nil) { 219 | self.fetchListUseCase = fetchListUseCase 220 | self.insertServiceUseCase = insertServiceUseCase 221 | } 222 | 223 | public func executeFetchList() { 224 | self.fetchListUseCase?.execute(value: ()) 225 | .assign(to: \.services, on: self) 226 | .store(in: &bag) 227 | } 228 | 229 | public func executeInsertService(serviceName: String?, 230 | secretKey: String?, 231 | additionalInfo: String?) { 232 | 233 | let value = InsertServiceRequestValue(serviceName: serviceName, 234 | secretKey: secretKey, 235 | additionalInfo: additionalInfo) 236 | self.insertServiceUseCase?.execute(value: value) 237 | .sink(receiveCompletion: { completion in 238 | switch completion { 239 | case .finished: 240 | break 241 | case .failure(let error): 242 | print(error.localizedDescription) 243 | break 244 | } 245 | }, receiveValue: { service in 246 | self.services.append(service) 247 | }) 248 | .store(in: &bag) 249 | } 250 | } 251 | ``` 252 | 253 | * ObservableObject로 구현 254 | * Domain Layer에 대한 의존성 255 | 256 | 257 | List로 service를 보여줄 TokenView 작성 258 | 259 | ``` swift 260 | import SwiftUI 261 | import DomainLayer 262 | 263 | public struct TokenView: View { 264 | //1 265 | @ObservedObject var viewModel: TokenViewModel 266 | 267 | public init(viewModel: TokenViewModel) { 268 | self.viewModel = viewModel 269 | } 270 | 271 | public var body: some View { 272 | NavigationView { 273 | List { 274 | //1 275 | ForEach(self.viewModel.services) { service in 276 | VStack(alignment: .leading) { 277 | Text(service.serviceName ?? "") 278 | Text(service.otpCode ?? "") 279 | .font(.title) 280 | .bold() 281 | Text(service.additinalInfo ?? "") 282 | } 283 | .padding() 284 | } 285 | } 286 | .navigationTitle("Tokens") 287 | .toolbar { 288 | ToolbarItem(placement: .primaryAction) { 289 | Button("Insert") { 290 | //2 291 | self.viewModel.executeInsertService(serviceName: "Token", 292 | secretKey: "123", 293 | additionalInfo: "insert@test.com") 294 | } 295 | } 296 | } 297 | } 298 | .onAppear { 299 | //3 300 | self.viewModel.executeFetchList() 301 | } 302 | } 303 | } 304 | ``` 305 | 1. ObservedObject로 선언된 ViewModel 내의 데이터가 업데이트되면 화면이 갱신됨 306 | 1. Insert 버튼 누르면 ViewModel의 insert UseCase를 실행 307 | 1. 화면이 보일때 ViewModel의 fetch list UseCase를 실행 308 | 309 | 310 | 311 | ## Data Layer 구현 312 | * DB, Network 등 내/외부 데이터를 사용하는 Layer 313 | * DataSource는 비동기로 동작하기 위해 [Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html) 로 구현 314 | * mocked data로 구현, data race를 방지하기 위해 actor 사용 315 | 316 | ``` swift 317 | import Foundation 318 | import DomainLayer 319 | 320 | public protocol ServiceDataSourceInterface { 321 | func insertService(value: InsertServiceRequestValue) async throws -> ServiceModel 322 | func fetchServiceList() async -> [ServiceModel] 323 | } 324 | 325 | public final actor ServiceMockDataSource { 326 | // 테스트 데이터 327 | var mockData: [ServiceModel] = [ 328 | ServiceModel(id: 0, otpCode: "123 123", serviceName: "Google", additinalInfo: "sample@google.com"), 329 | ServiceModel(id: 1, otpCode: "456 456", serviceName: "Github", additinalInfo: "sample@github.com"), 330 | ServiceModel(id: 2, otpCode: "789 789", serviceName: "Amazon", additinalInfo: "sample@amazon.com") 331 | ] 332 | 333 | public init() {} 334 | } 335 | 336 | extension ServiceMockDataSource: ServiceDataSourceInterface { 337 | 338 | public func insertService(value: InsertServiceRequestValue) async throws -> ServiceModel { 339 | guard let serviceName = value.serviceName else { throw ServiceError.unknown } 340 | 341 | let insertData = ServiceModel(id: Int64.random(in: 0.. [ServiceModel] { 352 | return mockData 353 | } 354 | 355 | } 356 | ``` 357 | 358 | * Combine operator에서 concurrency 호출을 위해 Future를 래핑하여 사용 359 | 360 | ``` swift 361 | import Combine 362 | 363 | extension Publisher { 364 | func asyncMap( 365 | _ transform: @escaping (Output) async -> T 366 | ) -> Publishers.FlatMap, Self> { 367 | flatMap { value in 368 | Future { promise in 369 | Task { 370 | let output = await transform(value) 371 | promise(.success(output)) 372 | } 373 | } 374 | } 375 | } 376 | 377 | func tryAsyncMap( 378 | _ transform: @escaping (Output) async throws -> T 379 | ) -> Publishers.FlatMap, Self> { 380 | flatMap { value in 381 | Future { promise in 382 | Task { 383 | do { 384 | let output = try await transform(value) 385 | promise(.success(output)) 386 | } catch { 387 | promise(.failure(error)) 388 | } 389 | } 390 | } 391 | } 392 | } 393 | } 394 | 395 | ``` 396 | 397 | * Domain Layer의 Repository Interface를 구현하여 Dependency Inversion을 완성 398 | 399 | ``` swift 400 | import Foundation 401 | import Combine 402 | import DomainLayer 403 | 404 | public struct ServiceRepository: ServiceRepositoryInterface { 405 | 406 | private let dataSource: ServiceDataSourceInterface 407 | 408 | public init(dataSource: ServiceDataSourceInterface) { 409 | self.dataSource = dataSource 410 | } 411 | 412 | public func insertService(value: InsertServiceRequestValue) -> AnyPublisher { 413 | return Just(value) 414 | .setFailureType(to: Error.self) 415 | .tryAsyncMap { try await dataSource.insertService(value: $0) } 416 | .receive(on: RunLoop.main) 417 | .eraseToAnyPublisher() 418 | } 419 | 420 | public func fetchServiceList() -> AnyPublisher<[ServiceModel], Never> { 421 | return Just(()) 422 | .asyncMap { await dataSource.fetchServiceList() } 423 | .receive(on: RunLoop.main) 424 | .eraseToAnyPublisher() 425 | } 426 | } 427 | ``` 428 | 429 | 430 | ## Dependency Injection 구현 431 | * 앱의 진입점에서 의존성 주입 및 환경 설정 432 | 433 | 434 | ![image](https://user-images.githubusercontent.com/25020477/155071395-f0d7b1ec-f644-46f4-9b5b-2a9da562655c.png) 435 | 436 | 437 | * AppDI Interface는 Presentation Layer에 구현 438 | 439 | ``` swift 440 | public protocol AppDIInterface { 441 | var tokenViewModel: TokenViewModel { get } 442 | } 443 | ``` 444 | 445 | * AppDI는 모든 DI를 사용하는 컨테이너 역할 446 | 447 | ``` swift 448 | import Foundation 449 | import DataLayer 450 | import DomainLayer 451 | import PresentationLayer 452 | 453 | enum PHASE { 454 | case DEV, ALPHA, REAL 455 | } 456 | 457 | public struct AppEnvironment { 458 | let phase: PHASE = .DEV 459 | } 460 | 461 | public class AppDI: AppDIInterface { 462 | 463 | static let shared = AppDI(appEnvironment: AppEnvironment()) 464 | 465 | private let appEnvironment: AppEnvironment 466 | 467 | private init(appEnvironment: AppEnvironment) { 468 | self.appEnvironment = appEnvironment 469 | } 470 | 471 | public lazy var tokenViewModel: TokenViewModel = { 472 | 473 | //MARK: Data Layer 474 | let dataSource: ServiceDataSourceInterface 475 | 476 | switch appEnvironment.phase { 477 | case .DEV: 478 | dataSource = ServiceMockDataSource() 479 | default: 480 | dataSource = ServiceMockDataSource() 481 | } 482 | 483 | let repository = ServiceRepository(dataSource: dataSource) 484 | 485 | //MARK: Domain Layer 486 | let fetchListUseCase = FetchServiceListUseCase(repository: repository) 487 | let insertServiceUseCase = InsertServiceUseCase(repository: repository) 488 | 489 | //MARK: Presentation 490 | let viewModel = TokenViewModel(fetchListUseCase: fetchListUseCase, 491 | insertServiceUseCase: insertServiceUseCase) 492 | 493 | return viewModel 494 | }() 495 | } 496 | ``` 497 | 498 | * 뷰 초기화 시 AppDI를 사용하여 의존성 주입 499 | 500 | ``` swift 501 | import SwiftUI 502 | import PresentationLayer 503 | 504 | @main 505 | struct HelloCleanArchitectureWithSwiftUIApp: App { 506 | var body: some Scene { 507 | WindowGroup { 508 | TokenView(viewModel: AppDI.shared.tokenViewModel) 509 | } 510 | } 511 | } 512 | ``` 513 | 514 | 515 | ## References 516 | * [Clean Coder Blog](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 517 | * [Clean Architecture for SwiftUI](https://medium.com/swlh/clean-architecture-for-swiftui-6d6c4eb1cf6a) 518 | * [SwiftUI를 위한 클린 아키텍처](https://gon125.github.io/posts/SwiftUI%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98/) 519 | * [[iOS, Swift] Clean Architecture With MVVM on iOS(using SwiftUI, Combine, SPM)](https://tigi44.github.io/ios/iOS,-Swift-Clean-Architecture-with-MVVM-DesignPattern-on-iOS/) 520 | * [[Clean Architecture] iOS Clean Architecture + MVVM 개념과 예제](https://eunjin3786.tistory.com/207) 521 | 522 | -------------------------------------------------------------------------------- /HelloCleanArchitectureWithSwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C2B50B66283B4ED300E90030 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = C2B50B65283B4ED300E90030 /* README.md */; }; 11 | D9BA919527C470E40041FBCB /* HelloCleanArchitectureWithSwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BA919427C470E40041FBCB /* HelloCleanArchitectureWithSwiftUIApp.swift */; }; 12 | D9BA919927C470E50041FBCB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D9BA919827C470E50041FBCB /* Assets.xcassets */; }; 13 | D9BA919C27C470E50041FBCB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D9BA919B27C470E50041FBCB /* Preview Assets.xcassets */; }; 14 | D9BA91A627C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BA91A527C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.swift */; }; 15 | D9BA91B027C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BA91AF27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.swift */; }; 16 | D9BA91C227C471B10041FBCB /* AppDI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BA91C127C471B10041FBCB /* AppDI.swift */; }; 17 | D9BA91C527C471D20041FBCB /* LayerPackage in Frameworks */ = {isa = PBXBuildFile; productRef = D9BA91C427C471D20041FBCB /* LayerPackage */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | D9BA91A227C470E60041FBCB /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = D9BA918927C470E40041FBCB /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = D9BA919027C470E40041FBCB; 26 | remoteInfo = HelloCleanArchitectureWithSwiftUI; 27 | }; 28 | D9BA91AC27C470E60041FBCB /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = D9BA918927C470E40041FBCB /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = D9BA919027C470E40041FBCB; 33 | remoteInfo = HelloCleanArchitectureWithSwiftUI; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | C2B50B65283B4ED300E90030 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 39 | D9BA919127C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloCleanArchitectureWithSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | D9BA919427C470E40041FBCB /* HelloCleanArchitectureWithSwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloCleanArchitectureWithSwiftUIApp.swift; sourceTree = ""; }; 41 | D9BA919827C470E50041FBCB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 42 | D9BA919B27C470E50041FBCB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 43 | D9BA91A127C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelloCleanArchitectureWithSwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | D9BA91A527C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloCleanArchitectureWithSwiftUITests.swift; sourceTree = ""; }; 45 | D9BA91AB27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelloCleanArchitectureWithSwiftUIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | D9BA91AF27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloCleanArchitectureWithSwiftUIUITests.swift; sourceTree = ""; }; 47 | D9BA91BF27C4715A0041FBCB /* LayerPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LayerPackage; sourceTree = ""; }; 48 | D9BA91C127C471B10041FBCB /* AppDI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDI.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | D9BA918E27C470E40041FBCB /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | D9BA91C527C471D20041FBCB /* LayerPackage in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | D9BA919E27C470E60041FBCB /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | D9BA91A827C470E60041FBCB /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | D9BA918827C470E40041FBCB = { 78 | isa = PBXGroup; 79 | children = ( 80 | D9BA91BE27C4715A0041FBCB /* Packages */, 81 | D9BA919327C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */, 82 | D9BA91A427C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests */, 83 | D9BA91AE27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests */, 84 | D9BA919227C470E40041FBCB /* Products */, 85 | D9BA91C327C471D20041FBCB /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | D9BA919227C470E40041FBCB /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | D9BA919127C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI.app */, 93 | D9BA91A127C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.xctest */, 94 | D9BA91AB27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | D9BA919327C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | C2B50B65283B4ED300E90030 /* README.md */, 103 | D9BA91C027C471B10041FBCB /* DependecyInjection */, 104 | D9BA919427C470E40041FBCB /* HelloCleanArchitectureWithSwiftUIApp.swift */, 105 | D9BA919827C470E50041FBCB /* Assets.xcassets */, 106 | D9BA919A27C470E50041FBCB /* Preview Content */, 107 | ); 108 | path = HelloCleanArchitectureWithSwiftUI; 109 | sourceTree = ""; 110 | }; 111 | D9BA919A27C470E50041FBCB /* Preview Content */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | D9BA919B27C470E50041FBCB /* Preview Assets.xcassets */, 115 | ); 116 | path = "Preview Content"; 117 | sourceTree = ""; 118 | }; 119 | D9BA91A427C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | D9BA91A527C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.swift */, 123 | ); 124 | path = HelloCleanArchitectureWithSwiftUITests; 125 | sourceTree = ""; 126 | }; 127 | D9BA91AE27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | D9BA91AF27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.swift */, 131 | ); 132 | path = HelloCleanArchitectureWithSwiftUIUITests; 133 | sourceTree = ""; 134 | }; 135 | D9BA91BE27C4715A0041FBCB /* Packages */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | D9BA91BF27C4715A0041FBCB /* LayerPackage */, 139 | ); 140 | name = Packages; 141 | sourceTree = ""; 142 | }; 143 | D9BA91C027C471B10041FBCB /* DependecyInjection */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | D9BA91C127C471B10041FBCB /* AppDI.swift */, 147 | ); 148 | path = DependecyInjection; 149 | sourceTree = ""; 150 | }; 151 | D9BA91C327C471D20041FBCB /* Frameworks */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | ); 155 | name = Frameworks; 156 | sourceTree = ""; 157 | }; 158 | /* End PBXGroup section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | D9BA919027C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = D9BA91B527C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUI" */; 164 | buildPhases = ( 165 | D9BA918D27C470E40041FBCB /* Sources */, 166 | D9BA918E27C470E40041FBCB /* Frameworks */, 167 | D9BA918F27C470E40041FBCB /* Resources */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | ); 173 | name = HelloCleanArchitectureWithSwiftUI; 174 | packageProductDependencies = ( 175 | D9BA91C427C471D20041FBCB /* LayerPackage */, 176 | ); 177 | productName = HelloCleanArchitectureWithSwiftUI; 178 | productReference = D9BA919127C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | D9BA91A027C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = D9BA91B827C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUITests" */; 184 | buildPhases = ( 185 | D9BA919D27C470E60041FBCB /* Sources */, 186 | D9BA919E27C470E60041FBCB /* Frameworks */, 187 | D9BA919F27C470E60041FBCB /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | D9BA91A327C470E60041FBCB /* PBXTargetDependency */, 193 | ); 194 | name = HelloCleanArchitectureWithSwiftUITests; 195 | productName = HelloCleanArchitectureWithSwiftUITests; 196 | productReference = D9BA91A127C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.xctest */; 197 | productType = "com.apple.product-type.bundle.unit-test"; 198 | }; 199 | D9BA91AA27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = D9BA91BB27C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUIUITests" */; 202 | buildPhases = ( 203 | D9BA91A727C470E60041FBCB /* Sources */, 204 | D9BA91A827C470E60041FBCB /* Frameworks */, 205 | D9BA91A927C470E60041FBCB /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | D9BA91AD27C470E60041FBCB /* PBXTargetDependency */, 211 | ); 212 | name = HelloCleanArchitectureWithSwiftUIUITests; 213 | productName = HelloCleanArchitectureWithSwiftUIUITests; 214 | productReference = D9BA91AB27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.xctest */; 215 | productType = "com.apple.product-type.bundle.ui-testing"; 216 | }; 217 | /* End PBXNativeTarget section */ 218 | 219 | /* Begin PBXProject section */ 220 | D9BA918927C470E40041FBCB /* Project object */ = { 221 | isa = PBXProject; 222 | attributes = { 223 | BuildIndependentTargetsInParallel = 1; 224 | LastSwiftUpdateCheck = 1320; 225 | LastUpgradeCheck = 1320; 226 | TargetAttributes = { 227 | D9BA919027C470E40041FBCB = { 228 | CreatedOnToolsVersion = 13.2.1; 229 | }; 230 | D9BA91A027C470E60041FBCB = { 231 | CreatedOnToolsVersion = 13.2.1; 232 | TestTargetID = D9BA919027C470E40041FBCB; 233 | }; 234 | D9BA91AA27C470E60041FBCB = { 235 | CreatedOnToolsVersion = 13.2.1; 236 | TestTargetID = D9BA919027C470E40041FBCB; 237 | }; 238 | }; 239 | }; 240 | buildConfigurationList = D9BA918C27C470E40041FBCB /* Build configuration list for PBXProject "HelloCleanArchitectureWithSwiftUI" */; 241 | compatibilityVersion = "Xcode 13.0"; 242 | developmentRegion = en; 243 | hasScannedForEncodings = 0; 244 | knownRegions = ( 245 | en, 246 | Base, 247 | ); 248 | mainGroup = D9BA918827C470E40041FBCB; 249 | productRefGroup = D9BA919227C470E40041FBCB /* Products */; 250 | projectDirPath = ""; 251 | projectRoot = ""; 252 | targets = ( 253 | D9BA919027C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */, 254 | D9BA91A027C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests */, 255 | D9BA91AA27C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests */, 256 | ); 257 | }; 258 | /* End PBXProject section */ 259 | 260 | /* Begin PBXResourcesBuildPhase section */ 261 | D9BA918F27C470E40041FBCB /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | C2B50B66283B4ED300E90030 /* README.md in Resources */, 266 | D9BA919C27C470E50041FBCB /* Preview Assets.xcassets in Resources */, 267 | D9BA919927C470E50041FBCB /* Assets.xcassets in Resources */, 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | D9BA919F27C470E60041FBCB /* Resources */ = { 272 | isa = PBXResourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | D9BA91A927C470E60041FBCB /* Resources */ = { 279 | isa = PBXResourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | }; 285 | /* End PBXResourcesBuildPhase section */ 286 | 287 | /* Begin PBXSourcesBuildPhase section */ 288 | D9BA918D27C470E40041FBCB /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | D9BA91C227C471B10041FBCB /* AppDI.swift in Sources */, 293 | D9BA919527C470E40041FBCB /* HelloCleanArchitectureWithSwiftUIApp.swift in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | D9BA919D27C470E60041FBCB /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | D9BA91A627C470E60041FBCB /* HelloCleanArchitectureWithSwiftUITests.swift in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | D9BA91A727C470E60041FBCB /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | D9BA91B027C470E60041FBCB /* HelloCleanArchitectureWithSwiftUIUITests.swift in Sources */, 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | }; 313 | /* End PBXSourcesBuildPhase section */ 314 | 315 | /* Begin PBXTargetDependency section */ 316 | D9BA91A327C470E60041FBCB /* PBXTargetDependency */ = { 317 | isa = PBXTargetDependency; 318 | target = D9BA919027C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */; 319 | targetProxy = D9BA91A227C470E60041FBCB /* PBXContainerItemProxy */; 320 | }; 321 | D9BA91AD27C470E60041FBCB /* PBXTargetDependency */ = { 322 | isa = PBXTargetDependency; 323 | target = D9BA919027C470E40041FBCB /* HelloCleanArchitectureWithSwiftUI */; 324 | targetProxy = D9BA91AC27C470E60041FBCB /* PBXContainerItemProxy */; 325 | }; 326 | /* End PBXTargetDependency section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | D9BA91B327C470E60041FBCB /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | COPY_PHASE_STRIP = NO; 363 | DEBUG_INFORMATION_FORMAT = dwarf; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | ENABLE_TESTABILITY = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu11; 367 | GCC_DYNAMIC_NO_PIC = NO; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_OPTIMIZATION_LEVEL = 0; 370 | GCC_PREPROCESSOR_DEFINITIONS = ( 371 | "DEBUG=1", 372 | "$(inherited)", 373 | ); 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 381 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 382 | MTL_FAST_MATH = YES; 383 | ONLY_ACTIVE_ARCH = YES; 384 | SDKROOT = iphoneos; 385 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 386 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 387 | }; 388 | name = Debug; 389 | }; 390 | D9BA91B427C470E60041FBCB /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | ALWAYS_SEARCH_USER_PATHS = NO; 394 | CLANG_ANALYZER_NONNULL = YES; 395 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 396 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 397 | CLANG_CXX_LIBRARY = "libc++"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_ENABLE_OBJC_WEAK = YES; 401 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_COMMA = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 406 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 407 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INFINITE_RECURSION = YES; 411 | CLANG_WARN_INT_CONVERSION = YES; 412 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 414 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 416 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | COPY_PHASE_STRIP = NO; 424 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 425 | ENABLE_NS_ASSERTIONS = NO; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu11; 428 | GCC_NO_COMMON_BLOCKS = YES; 429 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 430 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 431 | GCC_WARN_UNDECLARED_SELECTOR = YES; 432 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 433 | GCC_WARN_UNUSED_FUNCTION = YES; 434 | GCC_WARN_UNUSED_VARIABLE = YES; 435 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 436 | MTL_ENABLE_DEBUG_INFO = NO; 437 | MTL_FAST_MATH = YES; 438 | SDKROOT = iphoneos; 439 | SWIFT_COMPILATION_MODE = wholemodule; 440 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 441 | VALIDATE_PRODUCT = YES; 442 | }; 443 | name = Release; 444 | }; 445 | D9BA91B627C470E60041FBCB /* Debug */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 449 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 450 | CODE_SIGN_STYLE = Automatic; 451 | CURRENT_PROJECT_VERSION = 1; 452 | DEVELOPMENT_ASSET_PATHS = "\"HelloCleanArchitectureWithSwiftUI/Preview Content\""; 453 | DEVELOPMENT_TEAM = Q9WV86S859; 454 | ENABLE_PREVIEWS = YES; 455 | GENERATE_INFOPLIST_FILE = YES; 456 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 457 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 458 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/Frameworks", 464 | ); 465 | MARKETING_VERSION = 1.0; 466 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUI; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | D9BA91B727C470E60041FBCB /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_STYLE = Automatic; 480 | CURRENT_PROJECT_VERSION = 1; 481 | DEVELOPMENT_ASSET_PATHS = "\"HelloCleanArchitectureWithSwiftUI/Preview Content\""; 482 | DEVELOPMENT_TEAM = Q9WV86S859; 483 | ENABLE_PREVIEWS = YES; 484 | GENERATE_INFOPLIST_FILE = YES; 485 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 486 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 487 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 489 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 490 | LD_RUNPATH_SEARCH_PATHS = ( 491 | "$(inherited)", 492 | "@executable_path/Frameworks", 493 | ); 494 | MARKETING_VERSION = 1.0; 495 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUI; 496 | PRODUCT_NAME = "$(TARGET_NAME)"; 497 | SWIFT_EMIT_LOC_STRINGS = YES; 498 | SWIFT_VERSION = 5.0; 499 | TARGETED_DEVICE_FAMILY = "1,2"; 500 | }; 501 | name = Release; 502 | }; 503 | D9BA91B927C470E60041FBCB /* Debug */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 507 | BUNDLE_LOADER = "$(TEST_HOST)"; 508 | CODE_SIGN_STYLE = Automatic; 509 | CURRENT_PROJECT_VERSION = 1; 510 | DEVELOPMENT_TEAM = Q9WV86S859; 511 | GENERATE_INFOPLIST_FILE = YES; 512 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 513 | MARKETING_VERSION = 1.0; 514 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUITests; 515 | PRODUCT_NAME = "$(TARGET_NAME)"; 516 | SWIFT_EMIT_LOC_STRINGS = NO; 517 | SWIFT_VERSION = 5.0; 518 | TARGETED_DEVICE_FAMILY = "1,2"; 519 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HelloCleanArchitectureWithSwiftUI.app/HelloCleanArchitectureWithSwiftUI"; 520 | }; 521 | name = Debug; 522 | }; 523 | D9BA91BA27C470E60041FBCB /* Release */ = { 524 | isa = XCBuildConfiguration; 525 | buildSettings = { 526 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 527 | BUNDLE_LOADER = "$(TEST_HOST)"; 528 | CODE_SIGN_STYLE = Automatic; 529 | CURRENT_PROJECT_VERSION = 1; 530 | DEVELOPMENT_TEAM = Q9WV86S859; 531 | GENERATE_INFOPLIST_FILE = YES; 532 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 533 | MARKETING_VERSION = 1.0; 534 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUITests; 535 | PRODUCT_NAME = "$(TARGET_NAME)"; 536 | SWIFT_EMIT_LOC_STRINGS = NO; 537 | SWIFT_VERSION = 5.0; 538 | TARGETED_DEVICE_FAMILY = "1,2"; 539 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HelloCleanArchitectureWithSwiftUI.app/HelloCleanArchitectureWithSwiftUI"; 540 | }; 541 | name = Release; 542 | }; 543 | D9BA91BC27C470E60041FBCB /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 547 | CODE_SIGN_STYLE = Automatic; 548 | CURRENT_PROJECT_VERSION = 1; 549 | DEVELOPMENT_TEAM = Q9WV86S859; 550 | GENERATE_INFOPLIST_FILE = YES; 551 | MARKETING_VERSION = 1.0; 552 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUIUITests; 553 | PRODUCT_NAME = "$(TARGET_NAME)"; 554 | SWIFT_EMIT_LOC_STRINGS = NO; 555 | SWIFT_VERSION = 5.0; 556 | TARGETED_DEVICE_FAMILY = "1,2"; 557 | TEST_TARGET_NAME = HelloCleanArchitectureWithSwiftUI; 558 | }; 559 | name = Debug; 560 | }; 561 | D9BA91BD27C470E60041FBCB /* Release */ = { 562 | isa = XCBuildConfiguration; 563 | buildSettings = { 564 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 565 | CODE_SIGN_STYLE = Automatic; 566 | CURRENT_PROJECT_VERSION = 1; 567 | DEVELOPMENT_TEAM = Q9WV86S859; 568 | GENERATE_INFOPLIST_FILE = YES; 569 | MARKETING_VERSION = 1.0; 570 | PRODUCT_BUNDLE_IDENTIFIER = com.mindw.HelloCleanArchitectureWithSwiftUIUITests; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_EMIT_LOC_STRINGS = NO; 573 | SWIFT_VERSION = 5.0; 574 | TARGETED_DEVICE_FAMILY = "1,2"; 575 | TEST_TARGET_NAME = HelloCleanArchitectureWithSwiftUI; 576 | }; 577 | name = Release; 578 | }; 579 | /* End XCBuildConfiguration section */ 580 | 581 | /* Begin XCConfigurationList section */ 582 | D9BA918C27C470E40041FBCB /* Build configuration list for PBXProject "HelloCleanArchitectureWithSwiftUI" */ = { 583 | isa = XCConfigurationList; 584 | buildConfigurations = ( 585 | D9BA91B327C470E60041FBCB /* Debug */, 586 | D9BA91B427C470E60041FBCB /* Release */, 587 | ); 588 | defaultConfigurationIsVisible = 0; 589 | defaultConfigurationName = Release; 590 | }; 591 | D9BA91B527C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUI" */ = { 592 | isa = XCConfigurationList; 593 | buildConfigurations = ( 594 | D9BA91B627C470E60041FBCB /* Debug */, 595 | D9BA91B727C470E60041FBCB /* Release */, 596 | ); 597 | defaultConfigurationIsVisible = 0; 598 | defaultConfigurationName = Release; 599 | }; 600 | D9BA91B827C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUITests" */ = { 601 | isa = XCConfigurationList; 602 | buildConfigurations = ( 603 | D9BA91B927C470E60041FBCB /* Debug */, 604 | D9BA91BA27C470E60041FBCB /* Release */, 605 | ); 606 | defaultConfigurationIsVisible = 0; 607 | defaultConfigurationName = Release; 608 | }; 609 | D9BA91BB27C470E60041FBCB /* Build configuration list for PBXNativeTarget "HelloCleanArchitectureWithSwiftUIUITests" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | D9BA91BC27C470E60041FBCB /* Debug */, 613 | D9BA91BD27C470E60041FBCB /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | /* End XCConfigurationList section */ 619 | 620 | /* Begin XCSwiftPackageProductDependency section */ 621 | D9BA91C427C471D20041FBCB /* LayerPackage */ = { 622 | isa = XCSwiftPackageProductDependency; 623 | productName = LayerPackage; 624 | }; 625 | /* End XCSwiftPackageProductDependency section */ 626 | }; 627 | rootObject = D9BA918927C470E40041FBCB /* Project object */; 628 | } 629 | --------------------------------------------------------------------------------