├── 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 |
8 |
9 |
10 | ## Layer와 Data Flow
11 | 먼저 역할별 레이어들부터 알아보자면 다음과 같다.
12 | * Presentation Layer: UI 관련 레이어
13 | * Domain Layer: 비즈니스 룰과 로직 담당 레이어
14 | * Data Layer: 원격/로컬등 외부에서 데이터를 가져오는 레이어
15 |
16 | 
17 |
18 | * 각 레이어들의 Dependency 방향은 모두 원밖에서 원안쪽으로 향하고 있음
19 | * UI를 담당하는 Presentation Layer는 MVVM 패턴으로 구현됨
20 |
21 |
22 | 각 레이어의 데이터 흐름은 다음과 같다.
23 |
24 | 
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 |
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 | 
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 |
--------------------------------------------------------------------------------