├── A ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ └── A │ │ ├── A1Feature.swift │ │ ├── A1View.swift │ │ ├── A2Feature.swift │ │ └── A2View.swift └── Tests │ └── ATests │ └── ATests.swift ├── B1 ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ └── B1 │ │ ├── B1Feature.swift │ │ └── B1View.swift └── Tests │ └── B1Tests │ └── B1Tests.swift ├── B2 ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ └── B2 │ │ ├── B2Feature.swift │ │ └── SwiftUIView.swift └── Tests │ └── B2Tests │ └── B2Tests.swift ├── Effects ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ └── Effects │ │ └── Effects.swift └── Tests │ └── EffectsTests │ └── EffectsTests.swift ├── Modular-TCA.drawio ├── Modular-TCA ├── Modular-TCA.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── wimes.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── wimes.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── Modular-TCA │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── Sources │ │ ├── Login │ │ ├── LoginFeature.swift │ │ └── LoginView.swift │ │ ├── Modular_TCAApp.swift │ │ ├── Root │ │ ├── RootFeature.swift │ │ └── RootView.swift │ │ └── TabBar │ │ ├── TabBarFeature.swift │ │ └── TabBarView.swift ├── Modular-TCATests │ └── Modular_TCATests.swift └── Modular-TCAUITests │ ├── Modular_TCAUITests.swift │ └── Modular_TCAUITestsLaunchTests.swift ├── README.assets ├── app.gif ├── image-20220113233726026.png ├── image-20220113233800114.png ├── image-20220113235007940.png └── image-20220115003338295.png └── README.md /A/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /A/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /A/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-custom-dump", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 34 | "state": { 35 | "branch": null, 36 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 37 | "version": "0.3.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-identified-collections", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 43 | "state": { 44 | "branch": null, 45 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 46 | "version": "0.3.2" 47 | } 48 | }, 49 | { 50 | "package": "xctest-dynamic-overlay", 51 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 52 | "state": { 53 | "branch": null, 54 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 55 | "version": "0.2.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /A/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: "A", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "A", targets: ["A"]), 11 | ], 12 | dependencies: [ 13 | .package(name: "Effects", path: "../Effects") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "A", 18 | dependencies: ["Effects"]), 19 | .testTarget( 20 | name: "ATests", 21 | dependencies: ["A"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /A/README.md: -------------------------------------------------------------------------------- 1 | # A 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /A/Sources/A/A1Feature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // A1Feature.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/12. 6 | // 7 | 8 | import Effects 9 | import ComposableArchitecture 10 | 11 | public struct A1State: Equatable{ 12 | public init(){} 13 | var resultString: String = "" 14 | } 15 | 16 | public enum A1Action: Equatable{ 17 | case onAppear 18 | case dataLoaded(Result) 19 | } 20 | 21 | public struct A1Environment{ 22 | 23 | public init(){} 24 | 25 | var request: () -> Effect = { 26 | let effects: Effects = EffectsImpl() 27 | return effects.numbersApiOne() 28 | } 29 | 30 | var mainQueue: () -> AnySchedulerOf = { 31 | .main 32 | } 33 | } 34 | 35 | public let a1Reducer = Reducer< 36 | A1State, 37 | A1Action, 38 | A1Environment 39 | >{ state, action, environment in 40 | switch action{ 41 | case .onAppear: 42 | return environment.request() 43 | .receive(on: environment.mainQueue()) 44 | .catchToEffect() 45 | .map(A1Action.dataLoaded) 46 | case .dataLoaded(let result): 47 | switch result{ 48 | case .success(let result): 49 | state.resultString = result 50 | case .failure(let error): 51 | break 52 | } 53 | return .none 54 | } 55 | } 56 | 57 | func dummyA1Effect() -> Effect{ 58 | let dummyString = "test" 59 | return Effect(value: dummyString) 60 | } 61 | 62 | -------------------------------------------------------------------------------- /A/Sources/A/A1View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // A1View.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/12. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | import Effects 11 | 12 | public struct A1View: View { 13 | let store: Store 14 | 15 | public init(store: Store){ 16 | self.store = store 17 | } 18 | 19 | public var body: some View { 20 | WithViewStore(self.store){ viewStore in 21 | NavigationView{ 22 | VStack{ 23 | Text(viewStore.resultString) 24 | NavigationLink { 25 | A2View(store: Store( 26 | initialState: A2State(resultString: ""), 27 | reducer: a2Reducer, 28 | environment: A2Environment( 29 | request: {EffectsImpl().numbersApiThree()}, 30 | mainQueue: {.main} 31 | ))) 32 | } label: { 33 | Text("open the A2 View") 34 | } 35 | } 36 | .navigationTitle("A1") 37 | } 38 | .onAppear { 39 | viewStore.send(.onAppear) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /A/Sources/A/A2Feature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // A2Feature.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/12. 6 | // 7 | 8 | import Effects 9 | import ComposableArchitecture 10 | 11 | public struct A2State: Equatable{ 12 | var resultString: String 13 | } 14 | 15 | public enum A2Action: Equatable{ 16 | case onAppear 17 | case dataLoaded(Result) 18 | } 19 | 20 | public struct A2Environment{ 21 | var request: () -> Effect 22 | var mainQueue: () -> AnySchedulerOf 23 | 24 | public init( 25 | request: @escaping () -> Effect, 26 | mainQueue: @escaping () -> AnySchedulerOf 27 | ){ 28 | self.request = request 29 | self.mainQueue = mainQueue 30 | } 31 | } 32 | 33 | public let a2Reducer = Reducer< 34 | A2State, 35 | A2Action, 36 | A2Environment 37 | >{ state, action, environment in 38 | switch action{ 39 | case .onAppear: 40 | return environment.request() 41 | .receive(on: environment.mainQueue()) 42 | .catchToEffect() 43 | .map(A2Action.dataLoaded) 44 | case .dataLoaded(.success(let result)): 45 | state.resultString = result 46 | return .none 47 | case .dataLoaded(.failure(let error)): 48 | return .none 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /A/Sources/A/A2View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // A2View.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/12. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | public struct A2View: View { 12 | 13 | let store: Store 14 | 15 | public var body: some View { 16 | WithViewStore(self.store){ viewStore in 17 | VStack{ 18 | Text("\(viewStore.resultString)") 19 | } 20 | .navigationTitle("A2") 21 | .onAppear { 22 | viewStore.send(.onAppear) 23 | } 24 | } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /A/Tests/ATests/ATests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import A 3 | 4 | final class ATests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(A().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /B1/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /B1/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /B1/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-custom-dump", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 34 | "state": { 35 | "branch": null, 36 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 37 | "version": "0.3.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-identified-collections", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 43 | "state": { 44 | "branch": null, 45 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 46 | "version": "0.3.2" 47 | } 48 | }, 49 | { 50 | "package": "xctest-dynamic-overlay", 51 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 52 | "state": { 53 | "branch": null, 54 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 55 | "version": "0.2.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /B1/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: "B1", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "B1", targets: ["B1"]) 11 | ], 12 | dependencies: [ 13 | .package(name: "Effects", path: "../Effects"), 14 | .package(name: "B2", path: "../B2") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "B1", 19 | dependencies: ["Effects", "B2"]), 20 | .testTarget( 21 | name: "B1Tests", 22 | dependencies: ["B1"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /B1/README.md: -------------------------------------------------------------------------------- 1 | # B1 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /B1/Sources/B1/B1Feature.swift: -------------------------------------------------------------------------------- 1 | import Effects 2 | import ComposableArchitecture 3 | import B2 4 | 5 | public struct B1State: Equatable{ 6 | public var loginData: String = "" 7 | public var resultString: String = "" 8 | public var b2State = B2State(resultString: "") 9 | 10 | // 외부로 접근이 제한된 변수가 필요하다면 private으로 선언 11 | // private internalData: String = "" 12 | public init(){} 13 | } 14 | 15 | public enum B1Action{ 16 | case onAppear 17 | case dataLoaded(Result) 18 | 19 | case b2Action(B2Action) 20 | } 21 | 22 | public struct B1Environment{ 23 | var request: () -> Effect 24 | var mainQueue: () -> AnySchedulerOf 25 | 26 | public init( 27 | request: @escaping () -> Effect, 28 | mainQueue: @escaping () -> AnySchedulerOf 29 | ){ 30 | self.request = request 31 | self.mainQueue = mainQueue 32 | } 33 | } 34 | 35 | public let b1Reducer = Reducer< 36 | B1State, 37 | B1Action, 38 | B1Environment 39 | >.combine( 40 | b2Reducer.pullback( 41 | state: \.b2State, 42 | action: /B1Action.b2Action, 43 | environment: { _ in 44 | .init( 45 | request: EffectsImpl().numbersApiFour, 46 | mainQueue: {.main}) 47 | }), 48 | Reducer{ state, action, environment in 49 | switch action{ 50 | case .onAppear: 51 | return environment.request() 52 | .receive(on: environment.mainQueue()) 53 | .catchToEffect(B1Action.dataLoaded) 54 | case .dataLoaded(.success(let result)): 55 | state.resultString = result 56 | return .none 57 | case .dataLoaded(.failure(let error)): 58 | return .none 59 | default: 60 | return .none 61 | } 62 | } 63 | ) 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /B1/Sources/B1/B1View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // B1View.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/10. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | import B2 11 | 12 | public struct B1View: View { 13 | 14 | let store: Store 15 | 16 | public init(store: Store){ 17 | self.store = store 18 | } 19 | 20 | public var body: some View { 21 | WithViewStore(self.store){ viewStore in 22 | VStack{ 23 | Text("login Data: "+viewStore.loginData) 24 | Text(viewStore.resultString) 25 | NavigationLink { 26 | B2View(store: self.store.scope( 27 | state: \.b2State, 28 | action: B1Action.b2Action 29 | )) 30 | } label: { 31 | Text("Open the B2View") 32 | } 33 | } 34 | .navigationTitle("B1") 35 | .onAppear { 36 | viewStore.send(.onAppear) 37 | } 38 | } 39 | } 40 | } 41 | 42 | //struct SwiftUIView_Previews: PreviewProvider { 43 | // static var previews: some View { 44 | // SwiftUIView() 45 | // } 46 | //} 47 | -------------------------------------------------------------------------------- /B1/Tests/B1Tests/B1Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import B1 3 | 4 | final class B1Tests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(B1().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /B2/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /B2/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /B2/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-custom-dump", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 34 | "state": { 35 | "branch": null, 36 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 37 | "version": "0.3.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-identified-collections", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 43 | "state": { 44 | "branch": null, 45 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 46 | "version": "0.3.2" 47 | } 48 | }, 49 | { 50 | "package": "xctest-dynamic-overlay", 51 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 52 | "state": { 53 | "branch": null, 54 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 55 | "version": "0.2.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /B2/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: "B2", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "B2", targets: ["B2"]), 11 | ], 12 | dependencies: [ 13 | .package(name: "Effects", path: "../Effects") 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 17 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 18 | .target( 19 | name: "B2", 20 | dependencies: ["Effects"]), 21 | .testTarget( 22 | name: "B2Tests", 23 | dependencies: ["B2"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /B2/README.md: -------------------------------------------------------------------------------- 1 | # B2 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /B2/Sources/B2/B2Feature.swift: -------------------------------------------------------------------------------- 1 | import Effects 2 | import ComposableArchitecture 3 | 4 | public struct B2State: Equatable{ 5 | var resultString: String 6 | 7 | public init( 8 | resultString: String 9 | ){ 10 | self.resultString = resultString 11 | } 12 | } 13 | 14 | public enum B2Action{ 15 | // case receiveOpenView 16 | case onAppear 17 | case dataLoaded(Result) 18 | } 19 | 20 | public struct B2Environment{ 21 | var request: () -> Effect 22 | var mainQueue: () -> AnySchedulerOf 23 | 24 | public init( 25 | request: @escaping () -> Effect, 26 | mainQueue: @escaping () -> AnySchedulerOf 27 | ){ 28 | self.request = request 29 | self.mainQueue = mainQueue 30 | } 31 | } 32 | 33 | public let b2Reducer = Reducer< 34 | B2State, 35 | B2Action, 36 | B2Environment 37 | >{ state, action, environment in 38 | switch action{ 39 | // case .receiveOpenView: 40 | // return .none 41 | case .onAppear: 42 | print("### real B2 onAppear") 43 | return environment.request() 44 | .receive(on: environment.mainQueue()) 45 | .catchToEffect(B2Action.dataLoaded) 46 | 47 | case .dataLoaded(.success(let result)): 48 | state.resultString = result 49 | return .none 50 | case .dataLoaded(.failure(let error)): 51 | return .none 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /B2/Sources/B2/SwiftUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Wimes on 2022/01/12. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | public struct B2View: View { 12 | let store: Store 13 | 14 | public init(store: Store){ 15 | self.store = store 16 | } 17 | 18 | public var body: some View { 19 | WithViewStore(self.store){ viewStore in 20 | VStack{ 21 | Text(viewStore.resultString) 22 | } 23 | .navigationTitle("B2") 24 | .onAppear { 25 | viewStore.send(.onAppear) 26 | } 27 | } 28 | } 29 | } 30 | 31 | //struct SwiftUIView_Previews: PreviewProvider { 32 | // static var previews: some View { 33 | // SwiftUIView() 34 | // } 35 | //} 36 | -------------------------------------------------------------------------------- /B2/Tests/B2Tests/B2Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import B2 3 | 4 | final class B2Tests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(B2().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Effects/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Effects/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Effects/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-custom-dump", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 34 | "state": { 35 | "branch": null, 36 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 37 | "version": "0.3.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-identified-collections", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 43 | "state": { 44 | "branch": null, 45 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 46 | "version": "0.3.2" 47 | } 48 | }, 49 | { 50 | "package": "xctest-dynamic-overlay", 51 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 52 | "state": { 53 | "branch": null, 54 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 55 | "version": "0.2.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Effects/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: "Effects", 8 | platforms: [ 9 | .iOS(.v14) 10 | ], 11 | products: [ 12 | .library(name: "Effects", type: .dynamic, targets: ["Effects"]), 13 | ], 14 | dependencies: [ 15 | 16 | // MARK: https://forums.swift.org/t/how-to-integrate-tca-framework-as-of-0-1-3/36443 여기서는 안된다고 함. 17 | // 위 링크는 옛날 자료고 아래처럼 하면 됨 18 | // package( 19 | // url: "https://github.com/pointfreeco/swift-composable-architecture", 20 | // .upToNextMajor(from: "0.33.0") 21 | // ) 22 | .package( 23 | name: "swift-composable-architecture", 24 | path: "../swift-composable-architecture" 25 | ) 26 | ], 27 | targets: [ 28 | .target( 29 | name: "Effects", 30 | dependencies: [ 31 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 32 | ]), 33 | .testTarget( 34 | name: "EffectsTests", 35 | dependencies: ["Effects"]), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Effects/README.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Effects/Sources/Effects/Effects.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import ComposableArchitecture 4 | 5 | public enum ApiError: Error{ 6 | case downloadError 7 | case decodingError 8 | } 9 | 10 | public protocol Effects{ 11 | func numbersApiOne() -> Effect 12 | func numbersApiTwo() -> Effect 13 | func numbersApiThree() -> Effect 14 | func numbersApiFour() -> Effect 15 | } 16 | 17 | public class EffectsImpl: Effects{ 18 | public init(){} 19 | public func numbersApiOne() -> Effect{ 20 | guard let url = URL(string: "http://numbersapi.com/1") else{ 21 | fatalError("Error on creating url") 22 | } 23 | 24 | return URLSession.shared.dataTaskPublisher(for: url) 25 | .mapError{_ in ApiError.downloadError} 26 | .map(\.data) 27 | .compactMap{ String(data: $0, encoding: .utf8)} 28 | .eraseToEffect() 29 | } 30 | 31 | public func numbersApiTwo() -> Effect{ 32 | guard let url = URL(string: "http://numbersapi.com/2") else{ 33 | fatalError("Error on creating url") 34 | } 35 | 36 | return URLSession.shared.dataTaskPublisher(for: url) 37 | .mapError{_ in ApiError.downloadError} 38 | .map(\.data) 39 | .compactMap{ String(data: $0, encoding: .utf8)} 40 | .eraseToEffect() 41 | } 42 | 43 | public func numbersApiThree() -> Effect{ 44 | guard let url = URL(string: "http://numbersapi.com/3") else{ 45 | fatalError("Error on creating url") 46 | } 47 | 48 | return URLSession.shared.dataTaskPublisher(for: url) 49 | .mapError{_ in ApiError.downloadError} 50 | .map(\.data) 51 | .compactMap{ String(data: $0, encoding: .utf8)} 52 | .eraseToEffect() 53 | } 54 | 55 | public func numbersApiFour() -> Effect{ 56 | guard let url = URL(string: "http://numbersapi.com/4") else{ 57 | fatalError("Error on creating url") 58 | } 59 | 60 | return URLSession.shared.dataTaskPublisher(for: url) 61 | .mapError{_ in ApiError.downloadError} 62 | .map(\.data) 63 | .compactMap{ String(data: $0, encoding: .utf8)} 64 | .eraseToEffect() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Effects/Tests/EffectsTests/EffectsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Effects 3 | 4 | final class EffectsTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(Effects().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modular-TCA.drawio: -------------------------------------------------------------------------------- 1 | 7Vtdc5s4FP01fswOID4fEydNZ6eddjadbtM3DLLNFCOvwLG9v34hiK8rYsvYgOLuU6yLEHB0ztXVvcoETVe7R+qul5+Jj8OJpvi7CbqfaJqqKmb6J7Psc4ttaLlhQQOfdaoMT8G/mBkVZt0EPo4bHRNCwiRYN40eiSLsJQ2bSynZNrvNSdh86tpdYM7w5Lkhb/078JMl+wrNquwfcbBYFk9WTSe/snKLzuxL4qXrk23NhB4maEoJSfJfq90Uhxl4BS75fR/euFq+GMVRInKD5s22xu7D40f0Rdn+pHtnZm1ukJEP8+KGG/bF7G2TfQEBJZvIx9ko6gTdbZdBgp/Wrpdd3aaTntqWySpkl/m3Yi/6gmmCdzUTe8tHTFY4ofu0C7uq6QwxRhnTYe1tNQGqyWzLOvgWM7ps0hfl2BUu6Q8GzQkwlR8hE0xmE6ZSMjWYDKMFJg31BhOH0l8Zvd9GShkEKbsJFNJa+KS1AKX3hRPicPrmzu5cOjpSEKqyPRpUOgfVJ7IIIumQQubYSPGe/FYdHSYdEkofGyaTh+mQLx8GJlOXDSaLg+lOPjaN78ltHib52DQ+TA4HEwcSjvzbLF5PW17oxnHgNXFpgoh3QfKDXcl+P2f2PwzWut/Vut3vi0aUfsqPeqN2V9asbnttFfe9OSMx2VAPH4+HEpcucHKgH4sHsN/Yi/DzW4/sWqavsFEcuknw0tzBtM0pe8JXEqRfVtIHOU366CagRf7d7K76jgMMpAMe6ioYKAeGG+iVYuVnd2edKhC1XyPtkCDtDKloV+5fCq9ldKSdBviLhqYdH97/FrQT9Xa6VLS7Hm/Hb5UuRLuKas91ph2hXcW054K6vdBOf5eLbH/ezhmYdgIZxGv0dqKLrCUX7TTAlq7eDinA2ykD047feYnRLk5fLDmFjR2Z1YXF3dloCLLRlIqN17P2dt3gXicbLUE22lKx0QRLsgbLSaJsNI0jA/XMxsLH19j4MJ9jL4k5Ug6emgJFK7VYNkZLTRVK7V+6ZTRtI7P3eDob5CumQQoSpmcLuiy9H1N0MXWSSBqpR9YFUUlbYIFR4UrVt6T5nMKUrNYkdmcpSTXllnqZdr1kQ/HoIoexoaEbvMidQUXO741l8YgQLLOljD8sWIhPm0pQIoPHHaw2nIat4vMrhwQVVw6n0Uv4iPddEhTJ9CJfJg+feCclQ80V4jQ+n/gS/mfib0KX3nyb3p4H2DwIwykJCX29F/kutud5sEfJL1y7Yno2ns0vtAQUp/AYxI5ovRbmDC8HcdfsisDG9fQ49tQc9JtTcjzOFU0ma3Jlk5EFnD484iecSAH5QdMeNs5Flz8p8C6Ih0QTeAVDJSGerjRdl6l1JR4YyHAMIeKlTHD3tW7rrEN8QCmQ4JZy8L2QYxzqn/7I3+CiKiiWzt+tlIfa09MixT1jVBWogCRd3S9kmw2DqEupAD7HPqICoBrQ/2wV3Hz3vyvxn9tP88dV9Mv/58uDH930dnpnOBFwRD5HFcdFoMu0FDiwIigsAgTIJphr61sEuqUf6t+PCPgcw7kiGDweuowIhI95oDFFACuadud4CCScrf5qSK28k+wM23jOV3hHOKrzHZ13pzpfE6S9beWw84X9x47bW0Vz+RN479RZix6O+l80J4kGRiADiyBtVv+unHev/ukbPfwH -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 04B74F732789D95D009419AE /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B74F722789D95D009419AE /* LoginView.swift */; }; 11 | 04B74F7A2789D9B9009419AE /* TabBarFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B74F792789D9B9009419AE /* TabBarFeature.swift */; }; 12 | 04B74F7C2789D9D7009419AE /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B74F7B2789D9D7009419AE /* RootView.swift */; }; 13 | 04B74F7E2789DBE1009419AE /* LoginFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B74F7D2789DBE1009419AE /* LoginFeature.swift */; }; 14 | 04CE6AAB2789A66000D5D651 /* Modular_TCAApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6AAA2789A66000D5D651 /* Modular_TCAApp.swift */; }; 15 | 04CE6AAD2789A66000D5D651 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6AAC2789A66000D5D651 /* TabBarView.swift */; }; 16 | 04CE6AAF2789A66100D5D651 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04CE6AAE2789A66100D5D651 /* Assets.xcassets */; }; 17 | 04CE6AB22789A66100D5D651 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04CE6AB12789A66100D5D651 /* Preview Assets.xcassets */; }; 18 | 04CE6ABC2789A66100D5D651 /* Modular_TCATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6ABB2789A66100D5D651 /* Modular_TCATests.swift */; }; 19 | 04CE6AC62789A66100D5D651 /* Modular_TCAUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6AC52789A66100D5D651 /* Modular_TCAUITests.swift */; }; 20 | 04CE6AC82789A66100D5D651 /* Modular_TCAUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6AC72789A66100D5D651 /* Modular_TCAUITestsLaunchTests.swift */; }; 21 | 04CE6AD92789ADFD00D5D651 /* RootFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CE6AD82789ADFD00D5D651 /* RootFeature.swift */; }; 22 | 04D862BD278DB8E4004E82E1 /* B1 in Frameworks */ = {isa = PBXBuildFile; productRef = 04D862BC278DB8E4004E82E1 /* B1 */; }; 23 | 04F1F682278F189200EE0697 /* B2 in Frameworks */ = {isa = PBXBuildFile; productRef = 04F1F681278F189200EE0697 /* B2 */; }; 24 | 04F838FA278DD92000EA6D37 /* A in Frameworks */ = {isa = PBXBuildFile; productRef = 04F838F9278DD92000EA6D37 /* A */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | 04CE6AB82789A66100D5D651 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 04CE6A9F2789A66000D5D651 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 04CE6AA62789A66000D5D651; 33 | remoteInfo = "Modular-TCA"; 34 | }; 35 | 04CE6AC22789A66100D5D651 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 04CE6A9F2789A66000D5D651 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 04CE6AA62789A66000D5D651; 40 | remoteInfo = "Modular-TCA"; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXCopyFilesBuildPhase section */ 45 | 044C8503278C821E004F8CB3 /* Embed Frameworks */ = { 46 | isa = PBXCopyFilesBuildPhase; 47 | buildActionMask = 2147483647; 48 | dstPath = ""; 49 | dstSubfolderSpec = 10; 50 | files = ( 51 | ); 52 | name = "Embed Frameworks"; 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXCopyFilesBuildPhase section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 044C8504278C8226004F8CB3 /* B1 */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = B1; path = ../B1; sourceTree = ""; }; 59 | 04B74F722789D95D009419AE /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 60 | 04B74F792789D9B9009419AE /* TabBarFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarFeature.swift; sourceTree = ""; }; 61 | 04B74F7B2789D9D7009419AE /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 62 | 04B74F7D2789DBE1009419AE /* LoginFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFeature.swift; sourceTree = ""; }; 63 | 04CE6AA72789A66000D5D651 /* Modular-TCA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Modular-TCA.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 04CE6AAA2789A66000D5D651 /* Modular_TCAApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modular_TCAApp.swift; sourceTree = ""; }; 65 | 04CE6AAC2789A66000D5D651 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 66 | 04CE6AAE2789A66100D5D651 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 67 | 04CE6AB12789A66100D5D651 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 68 | 04CE6AB72789A66100D5D651 /* Modular-TCATests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Modular-TCATests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | 04CE6ABB2789A66100D5D651 /* Modular_TCATests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modular_TCATests.swift; sourceTree = ""; }; 70 | 04CE6AC12789A66100D5D651 /* Modular-TCAUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Modular-TCAUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | 04CE6AC52789A66100D5D651 /* Modular_TCAUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modular_TCAUITests.swift; sourceTree = ""; }; 72 | 04CE6AC72789A66100D5D651 /* Modular_TCAUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modular_TCAUITestsLaunchTests.swift; sourceTree = ""; }; 73 | 04CE6AD82789ADFD00D5D651 /* RootFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootFeature.swift; sourceTree = ""; }; 74 | 04CE6AE42789CE6100D5D651 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 75 | 04F1F680278F188C00EE0697 /* B2 */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = B2; path = ../B2; sourceTree = ""; }; 76 | 04F838F8278DD91300EA6D37 /* A */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = A; path = ../A; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 04CE6AA42789A66000D5D651 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 04F838FA278DD92000EA6D37 /* A in Frameworks */, 85 | 04D862BD278DB8E4004E82E1 /* B1 in Frameworks */, 86 | 04F1F682278F189200EE0697 /* B2 in Frameworks */, 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | 04CE6AB42789A66100D5D651 /* Frameworks */ = { 91 | isa = PBXFrameworksBuildPhase; 92 | buildActionMask = 2147483647; 93 | files = ( 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 04CE6ABE2789A66100D5D651 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | /* End PBXFrameworksBuildPhase section */ 105 | 106 | /* Begin PBXGroup section */ 107 | 04B74F742789D966009419AE /* Sources */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 04CE6AAA2789A66000D5D651 /* Modular_TCAApp.swift */, 111 | 04B74F772789D986009419AE /* Root */, 112 | 04B74F762789D982009419AE /* Login */, 113 | 04B74F752789D975009419AE /* TabBar */, 114 | ); 115 | path = Sources; 116 | sourceTree = ""; 117 | }; 118 | 04B74F752789D975009419AE /* TabBar */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 04CE6AAC2789A66000D5D651 /* TabBarView.swift */, 122 | 04B74F792789D9B9009419AE /* TabBarFeature.swift */, 123 | ); 124 | path = TabBar; 125 | sourceTree = ""; 126 | }; 127 | 04B74F762789D982009419AE /* Login */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 04B74F722789D95D009419AE /* LoginView.swift */, 131 | 04B74F7D2789DBE1009419AE /* LoginFeature.swift */, 132 | ); 133 | path = Login; 134 | sourceTree = ""; 135 | }; 136 | 04B74F772789D986009419AE /* Root */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 04B74F7B2789D9D7009419AE /* RootView.swift */, 140 | 04CE6AD82789ADFD00D5D651 /* RootFeature.swift */, 141 | ); 142 | path = Root; 143 | sourceTree = ""; 144 | }; 145 | 04CE6A9E2789A66000D5D651 = { 146 | isa = PBXGroup; 147 | children = ( 148 | 04F1F680278F188C00EE0697 /* B2 */, 149 | 044C8504278C8226004F8CB3 /* B1 */, 150 | 04F838F8278DD91300EA6D37 /* A */, 151 | 04CE6AA92789A66000D5D651 /* Modular-TCA */, 152 | 04CE6ABA2789A66100D5D651 /* Modular-TCATests */, 153 | 04CE6AC42789A66100D5D651 /* Modular-TCAUITests */, 154 | 04CE6AA82789A66000D5D651 /* Products */, 155 | 04CE6AD52789A9CE00D5D651 /* Frameworks */, 156 | ); 157 | sourceTree = ""; 158 | }; 159 | 04CE6AA82789A66000D5D651 /* Products */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 04CE6AA72789A66000D5D651 /* Modular-TCA.app */, 163 | 04CE6AB72789A66100D5D651 /* Modular-TCATests.xctest */, 164 | 04CE6AC12789A66100D5D651 /* Modular-TCAUITests.xctest */, 165 | ); 166 | name = Products; 167 | sourceTree = ""; 168 | }; 169 | 04CE6AA92789A66000D5D651 /* Modular-TCA */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 04B74F742789D966009419AE /* Sources */, 173 | 04CE6AE42789CE6100D5D651 /* Info.plist */, 174 | 04CE6AAE2789A66100D5D651 /* Assets.xcassets */, 175 | 04CE6AB02789A66100D5D651 /* Preview Content */, 176 | ); 177 | path = "Modular-TCA"; 178 | sourceTree = ""; 179 | }; 180 | 04CE6AB02789A66100D5D651 /* Preview Content */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 04CE6AB12789A66100D5D651 /* Preview Assets.xcassets */, 184 | ); 185 | path = "Preview Content"; 186 | sourceTree = ""; 187 | }; 188 | 04CE6ABA2789A66100D5D651 /* Modular-TCATests */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 04CE6ABB2789A66100D5D651 /* Modular_TCATests.swift */, 192 | ); 193 | path = "Modular-TCATests"; 194 | sourceTree = ""; 195 | }; 196 | 04CE6AC42789A66100D5D651 /* Modular-TCAUITests */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 04CE6AC52789A66100D5D651 /* Modular_TCAUITests.swift */, 200 | 04CE6AC72789A66100D5D651 /* Modular_TCAUITestsLaunchTests.swift */, 201 | ); 202 | path = "Modular-TCAUITests"; 203 | sourceTree = ""; 204 | }; 205 | 04CE6AD52789A9CE00D5D651 /* Frameworks */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | ); 209 | name = Frameworks; 210 | sourceTree = ""; 211 | }; 212 | /* End PBXGroup section */ 213 | 214 | /* Begin PBXNativeTarget section */ 215 | 04CE6AA62789A66000D5D651 /* Modular-TCA */ = { 216 | isa = PBXNativeTarget; 217 | buildConfigurationList = 04CE6ACB2789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCA" */; 218 | buildPhases = ( 219 | 04CE6AA32789A66000D5D651 /* Sources */, 220 | 04CE6AA42789A66000D5D651 /* Frameworks */, 221 | 04CE6AA52789A66000D5D651 /* Resources */, 222 | 044C8503278C821E004F8CB3 /* Embed Frameworks */, 223 | ); 224 | buildRules = ( 225 | ); 226 | dependencies = ( 227 | ); 228 | name = "Modular-TCA"; 229 | packageProductDependencies = ( 230 | 04D862BC278DB8E4004E82E1 /* B1 */, 231 | 04F838F9278DD92000EA6D37 /* A */, 232 | 04F1F681278F189200EE0697 /* B2 */, 233 | ); 234 | productName = "Modular-TCA"; 235 | productReference = 04CE6AA72789A66000D5D651 /* Modular-TCA.app */; 236 | productType = "com.apple.product-type.application"; 237 | }; 238 | 04CE6AB62789A66100D5D651 /* Modular-TCATests */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = 04CE6ACE2789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCATests" */; 241 | buildPhases = ( 242 | 04CE6AB32789A66100D5D651 /* Sources */, 243 | 04CE6AB42789A66100D5D651 /* Frameworks */, 244 | 04CE6AB52789A66100D5D651 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | 04CE6AB92789A66100D5D651 /* PBXTargetDependency */, 250 | ); 251 | name = "Modular-TCATests"; 252 | productName = "Modular-TCATests"; 253 | productReference = 04CE6AB72789A66100D5D651 /* Modular-TCATests.xctest */; 254 | productType = "com.apple.product-type.bundle.unit-test"; 255 | }; 256 | 04CE6AC02789A66100D5D651 /* Modular-TCAUITests */ = { 257 | isa = PBXNativeTarget; 258 | buildConfigurationList = 04CE6AD12789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCAUITests" */; 259 | buildPhases = ( 260 | 04CE6ABD2789A66100D5D651 /* Sources */, 261 | 04CE6ABE2789A66100D5D651 /* Frameworks */, 262 | 04CE6ABF2789A66100D5D651 /* Resources */, 263 | ); 264 | buildRules = ( 265 | ); 266 | dependencies = ( 267 | 04CE6AC32789A66100D5D651 /* PBXTargetDependency */, 268 | ); 269 | name = "Modular-TCAUITests"; 270 | productName = "Modular-TCAUITests"; 271 | productReference = 04CE6AC12789A66100D5D651 /* Modular-TCAUITests.xctest */; 272 | productType = "com.apple.product-type.bundle.ui-testing"; 273 | }; 274 | /* End PBXNativeTarget section */ 275 | 276 | /* Begin PBXProject section */ 277 | 04CE6A9F2789A66000D5D651 /* Project object */ = { 278 | isa = PBXProject; 279 | attributes = { 280 | BuildIndependentTargetsInParallel = 1; 281 | LastSwiftUpdateCheck = 1320; 282 | LastUpgradeCheck = 1320; 283 | TargetAttributes = { 284 | 04CE6AA62789A66000D5D651 = { 285 | CreatedOnToolsVersion = 13.2.1; 286 | }; 287 | 04CE6AB62789A66100D5D651 = { 288 | CreatedOnToolsVersion = 13.2.1; 289 | TestTargetID = 04CE6AA62789A66000D5D651; 290 | }; 291 | 04CE6AC02789A66100D5D651 = { 292 | CreatedOnToolsVersion = 13.2.1; 293 | TestTargetID = 04CE6AA62789A66000D5D651; 294 | }; 295 | }; 296 | }; 297 | buildConfigurationList = 04CE6AA22789A66000D5D651 /* Build configuration list for PBXProject "Modular-TCA" */; 298 | compatibilityVersion = "Xcode 13.0"; 299 | developmentRegion = en; 300 | hasScannedForEncodings = 0; 301 | knownRegions = ( 302 | en, 303 | Base, 304 | ); 305 | mainGroup = 04CE6A9E2789A66000D5D651; 306 | productRefGroup = 04CE6AA82789A66000D5D651 /* Products */; 307 | projectDirPath = ""; 308 | projectRoot = ""; 309 | targets = ( 310 | 04CE6AA62789A66000D5D651 /* Modular-TCA */, 311 | 04CE6AB62789A66100D5D651 /* Modular-TCATests */, 312 | 04CE6AC02789A66100D5D651 /* Modular-TCAUITests */, 313 | ); 314 | }; 315 | /* End PBXProject section */ 316 | 317 | /* Begin PBXResourcesBuildPhase section */ 318 | 04CE6AA52789A66000D5D651 /* Resources */ = { 319 | isa = PBXResourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 04CE6AB22789A66100D5D651 /* Preview Assets.xcassets in Resources */, 323 | 04CE6AAF2789A66100D5D651 /* Assets.xcassets in Resources */, 324 | ); 325 | runOnlyForDeploymentPostprocessing = 0; 326 | }; 327 | 04CE6AB52789A66100D5D651 /* Resources */ = { 328 | isa = PBXResourcesBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | }; 334 | 04CE6ABF2789A66100D5D651 /* Resources */ = { 335 | isa = PBXResourcesBuildPhase; 336 | buildActionMask = 2147483647; 337 | files = ( 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | /* End PBXResourcesBuildPhase section */ 342 | 343 | /* Begin PBXSourcesBuildPhase section */ 344 | 04CE6AA32789A66000D5D651 /* Sources */ = { 345 | isa = PBXSourcesBuildPhase; 346 | buildActionMask = 2147483647; 347 | files = ( 348 | 04B74F7A2789D9B9009419AE /* TabBarFeature.swift in Sources */, 349 | 04CE6AAD2789A66000D5D651 /* TabBarView.swift in Sources */, 350 | 04B74F7E2789DBE1009419AE /* LoginFeature.swift in Sources */, 351 | 04CE6AD92789ADFD00D5D651 /* RootFeature.swift in Sources */, 352 | 04B74F7C2789D9D7009419AE /* RootView.swift in Sources */, 353 | 04B74F732789D95D009419AE /* LoginView.swift in Sources */, 354 | 04CE6AAB2789A66000D5D651 /* Modular_TCAApp.swift in Sources */, 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | 04CE6AB32789A66100D5D651 /* Sources */ = { 359 | isa = PBXSourcesBuildPhase; 360 | buildActionMask = 2147483647; 361 | files = ( 362 | 04CE6ABC2789A66100D5D651 /* Modular_TCATests.swift in Sources */, 363 | ); 364 | runOnlyForDeploymentPostprocessing = 0; 365 | }; 366 | 04CE6ABD2789A66100D5D651 /* Sources */ = { 367 | isa = PBXSourcesBuildPhase; 368 | buildActionMask = 2147483647; 369 | files = ( 370 | 04CE6AC82789A66100D5D651 /* Modular_TCAUITestsLaunchTests.swift in Sources */, 371 | 04CE6AC62789A66100D5D651 /* Modular_TCAUITests.swift in Sources */, 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | /* End PBXSourcesBuildPhase section */ 376 | 377 | /* Begin PBXTargetDependency section */ 378 | 04CE6AB92789A66100D5D651 /* PBXTargetDependency */ = { 379 | isa = PBXTargetDependency; 380 | target = 04CE6AA62789A66000D5D651 /* Modular-TCA */; 381 | targetProxy = 04CE6AB82789A66100D5D651 /* PBXContainerItemProxy */; 382 | }; 383 | 04CE6AC32789A66100D5D651 /* PBXTargetDependency */ = { 384 | isa = PBXTargetDependency; 385 | target = 04CE6AA62789A66000D5D651 /* Modular-TCA */; 386 | targetProxy = 04CE6AC22789A66100D5D651 /* PBXContainerItemProxy */; 387 | }; 388 | /* End PBXTargetDependency section */ 389 | 390 | /* Begin XCBuildConfiguration section */ 391 | 04CE6AC92789A66100D5D651 /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 398 | CLANG_CXX_LIBRARY = "libc++"; 399 | CLANG_ENABLE_MODULES = YES; 400 | CLANG_ENABLE_OBJC_ARC = YES; 401 | CLANG_ENABLE_OBJC_WEAK = YES; 402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 403 | CLANG_WARN_BOOL_CONVERSION = YES; 404 | CLANG_WARN_COMMA = YES; 405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | COPY_PHASE_STRIP = NO; 425 | DEBUG_INFORMATION_FORMAT = dwarf; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | ENABLE_TESTABILITY = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu11; 429 | GCC_DYNAMIC_NO_PIC = NO; 430 | GCC_NO_COMMON_BLOCKS = YES; 431 | GCC_OPTIMIZATION_LEVEL = 0; 432 | GCC_PREPROCESSOR_DEFINITIONS = ( 433 | "DEBUG=1", 434 | "$(inherited)", 435 | ); 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 443 | MACH_O_TYPE = mh_execute; 444 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 445 | MTL_FAST_MATH = YES; 446 | ONLY_ACTIVE_ARCH = YES; 447 | SDKROOT = iphoneos; 448 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 449 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 450 | }; 451 | name = Debug; 452 | }; 453 | 04CE6ACA2789A66100D5D651 /* Release */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 459 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 460 | CLANG_CXX_LIBRARY = "libc++"; 461 | CLANG_ENABLE_MODULES = YES; 462 | CLANG_ENABLE_OBJC_ARC = YES; 463 | CLANG_ENABLE_OBJC_WEAK = YES; 464 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 465 | CLANG_WARN_BOOL_CONVERSION = YES; 466 | CLANG_WARN_COMMA = YES; 467 | CLANG_WARN_CONSTANT_CONVERSION = YES; 468 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 470 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 471 | CLANG_WARN_EMPTY_BODY = YES; 472 | CLANG_WARN_ENUM_CONVERSION = YES; 473 | CLANG_WARN_INFINITE_RECURSION = YES; 474 | CLANG_WARN_INT_CONVERSION = YES; 475 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 476 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 477 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 478 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 479 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 480 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 481 | CLANG_WARN_STRICT_PROTOTYPES = YES; 482 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 483 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 484 | CLANG_WARN_UNREACHABLE_CODE = YES; 485 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 486 | COPY_PHASE_STRIP = NO; 487 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 488 | ENABLE_NS_ASSERTIONS = NO; 489 | ENABLE_STRICT_OBJC_MSGSEND = YES; 490 | GCC_C_LANGUAGE_STANDARD = gnu11; 491 | GCC_NO_COMMON_BLOCKS = YES; 492 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 493 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 494 | GCC_WARN_UNDECLARED_SELECTOR = YES; 495 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 496 | GCC_WARN_UNUSED_FUNCTION = YES; 497 | GCC_WARN_UNUSED_VARIABLE = YES; 498 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 499 | MACH_O_TYPE = mh_execute; 500 | MTL_ENABLE_DEBUG_INFO = NO; 501 | MTL_FAST_MATH = YES; 502 | SDKROOT = iphoneos; 503 | SWIFT_COMPILATION_MODE = wholemodule; 504 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 505 | VALIDATE_PRODUCT = YES; 506 | }; 507 | name = Release; 508 | }; 509 | 04CE6ACC2789A66100D5D651 /* Debug */ = { 510 | isa = XCBuildConfiguration; 511 | buildSettings = { 512 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 513 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 514 | CODE_SIGN_STYLE = Automatic; 515 | CURRENT_PROJECT_VERSION = 1; 516 | DEVELOPMENT_ASSET_PATHS = "\"Modular-TCA/Preview Content\""; 517 | ENABLE_PREVIEWS = YES; 518 | GENERATE_INFOPLIST_FILE = YES; 519 | INFOPLIST_FILE = "Modular-TCA/Info.plist"; 520 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 521 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 522 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 523 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 524 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 525 | LD_RUNPATH_SEARCH_PATHS = ( 526 | "$(inherited)", 527 | "@executable_path/Frameworks", 528 | ); 529 | MARKETING_VERSION = 1.0; 530 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCA"; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_EMIT_LOC_STRINGS = YES; 533 | SWIFT_VERSION = 5.0; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | }; 536 | name = Debug; 537 | }; 538 | 04CE6ACD2789A66100D5D651 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 543 | CODE_SIGN_STYLE = Automatic; 544 | CURRENT_PROJECT_VERSION = 1; 545 | DEVELOPMENT_ASSET_PATHS = "\"Modular-TCA/Preview Content\""; 546 | ENABLE_PREVIEWS = YES; 547 | GENERATE_INFOPLIST_FILE = YES; 548 | INFOPLIST_FILE = "Modular-TCA/Info.plist"; 549 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 550 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 551 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 552 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 553 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 554 | LD_RUNPATH_SEARCH_PATHS = ( 555 | "$(inherited)", 556 | "@executable_path/Frameworks", 557 | ); 558 | MARKETING_VERSION = 1.0; 559 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCA"; 560 | PRODUCT_NAME = "$(TARGET_NAME)"; 561 | SWIFT_EMIT_LOC_STRINGS = YES; 562 | SWIFT_VERSION = 5.0; 563 | TARGETED_DEVICE_FAMILY = "1,2"; 564 | }; 565 | name = Release; 566 | }; 567 | 04CE6ACF2789A66100D5D651 /* Debug */ = { 568 | isa = XCBuildConfiguration; 569 | buildSettings = { 570 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 571 | BUNDLE_LOADER = "$(TEST_HOST)"; 572 | CODE_SIGN_STYLE = Automatic; 573 | CURRENT_PROJECT_VERSION = 1; 574 | GENERATE_INFOPLIST_FILE = YES; 575 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 576 | MARKETING_VERSION = 1.0; 577 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCATests"; 578 | PRODUCT_NAME = "$(TARGET_NAME)"; 579 | SWIFT_EMIT_LOC_STRINGS = NO; 580 | SWIFT_VERSION = 5.0; 581 | TARGETED_DEVICE_FAMILY = "1,2"; 582 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Modular-TCA.app/Modular-TCA"; 583 | }; 584 | name = Debug; 585 | }; 586 | 04CE6AD02789A66100D5D651 /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 590 | BUNDLE_LOADER = "$(TEST_HOST)"; 591 | CODE_SIGN_STYLE = Automatic; 592 | CURRENT_PROJECT_VERSION = 1; 593 | GENERATE_INFOPLIST_FILE = YES; 594 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 595 | MARKETING_VERSION = 1.0; 596 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCATests"; 597 | PRODUCT_NAME = "$(TARGET_NAME)"; 598 | SWIFT_EMIT_LOC_STRINGS = NO; 599 | SWIFT_VERSION = 5.0; 600 | TARGETED_DEVICE_FAMILY = "1,2"; 601 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Modular-TCA.app/Modular-TCA"; 602 | }; 603 | name = Release; 604 | }; 605 | 04CE6AD22789A66100D5D651 /* Debug */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 609 | CODE_SIGN_STYLE = Automatic; 610 | CURRENT_PROJECT_VERSION = 1; 611 | GENERATE_INFOPLIST_FILE = YES; 612 | MARKETING_VERSION = 1.0; 613 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCAUITests"; 614 | PRODUCT_NAME = "$(TARGET_NAME)"; 615 | SWIFT_EMIT_LOC_STRINGS = NO; 616 | SWIFT_VERSION = 5.0; 617 | TARGETED_DEVICE_FAMILY = "1,2"; 618 | TEST_TARGET_NAME = "Modular-TCA"; 619 | }; 620 | name = Debug; 621 | }; 622 | 04CE6AD32789A66100D5D651 /* Release */ = { 623 | isa = XCBuildConfiguration; 624 | buildSettings = { 625 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 626 | CODE_SIGN_STYLE = Automatic; 627 | CURRENT_PROJECT_VERSION = 1; 628 | GENERATE_INFOPLIST_FILE = YES; 629 | MARKETING_VERSION = 1.0; 630 | PRODUCT_BUNDLE_IDENTIFIER = "com.wimes.tests.Modular-TCAUITests"; 631 | PRODUCT_NAME = "$(TARGET_NAME)"; 632 | SWIFT_EMIT_LOC_STRINGS = NO; 633 | SWIFT_VERSION = 5.0; 634 | TARGETED_DEVICE_FAMILY = "1,2"; 635 | TEST_TARGET_NAME = "Modular-TCA"; 636 | }; 637 | name = Release; 638 | }; 639 | /* End XCBuildConfiguration section */ 640 | 641 | /* Begin XCConfigurationList section */ 642 | 04CE6AA22789A66000D5D651 /* Build configuration list for PBXProject "Modular-TCA" */ = { 643 | isa = XCConfigurationList; 644 | buildConfigurations = ( 645 | 04CE6AC92789A66100D5D651 /* Debug */, 646 | 04CE6ACA2789A66100D5D651 /* Release */, 647 | ); 648 | defaultConfigurationIsVisible = 0; 649 | defaultConfigurationName = Release; 650 | }; 651 | 04CE6ACB2789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCA" */ = { 652 | isa = XCConfigurationList; 653 | buildConfigurations = ( 654 | 04CE6ACC2789A66100D5D651 /* Debug */, 655 | 04CE6ACD2789A66100D5D651 /* Release */, 656 | ); 657 | defaultConfigurationIsVisible = 0; 658 | defaultConfigurationName = Release; 659 | }; 660 | 04CE6ACE2789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCATests" */ = { 661 | isa = XCConfigurationList; 662 | buildConfigurations = ( 663 | 04CE6ACF2789A66100D5D651 /* Debug */, 664 | 04CE6AD02789A66100D5D651 /* Release */, 665 | ); 666 | defaultConfigurationIsVisible = 0; 667 | defaultConfigurationName = Release; 668 | }; 669 | 04CE6AD12789A66100D5D651 /* Build configuration list for PBXNativeTarget "Modular-TCAUITests" */ = { 670 | isa = XCConfigurationList; 671 | buildConfigurations = ( 672 | 04CE6AD22789A66100D5D651 /* Debug */, 673 | 04CE6AD32789A66100D5D651 /* Release */, 674 | ); 675 | defaultConfigurationIsVisible = 0; 676 | defaultConfigurationName = Release; 677 | }; 678 | /* End XCConfigurationList section */ 679 | 680 | /* Begin XCSwiftPackageProductDependency section */ 681 | 04D862BC278DB8E4004E82E1 /* B1 */ = { 682 | isa = XCSwiftPackageProductDependency; 683 | productName = B1; 684 | }; 685 | 04F1F681278F189200EE0697 /* B2 */ = { 686 | isa = XCSwiftPackageProductDependency; 687 | productName = B2; 688 | }; 689 | 04F838F9278DD92000EA6D37 /* A */ = { 690 | isa = XCSwiftPackageProductDependency; 691 | productName = A; 692 | }; 693 | /* End XCSwiftPackageProductDependency section */ 694 | }; 695 | rootObject = 04CE6A9F2789A66000D5D651 /* Project object */; 696 | } 697 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 10 | "version": "0.5.3" 11 | } 12 | }, 13 | { 14 | "package": "swift-case-paths", 15 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 16 | "state": { 17 | "branch": null, 18 | "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", 19 | "version": "0.8.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "https://github.com/apple/swift-collections", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-custom-dump", 33 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 34 | "state": { 35 | "branch": null, 36 | "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", 37 | "version": "0.3.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-identified-collections", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 43 | "state": { 44 | "branch": null, 45 | "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", 46 | "version": "0.3.2" 47 | } 48 | }, 49 | { 50 | "package": "xctest-dynamic-overlay", 51 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 52 | "state": { 53 | "branch": null, 54 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 55 | "version": "0.2.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/project.xcworkspace/xcuserdata/wimes.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/Modular-TCA/Modular-TCA.xcodeproj/project.xcworkspace/xcuserdata/wimes.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA.xcodeproj/xcuserdata/wimes.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CustomDump (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 2 13 | 14 | CustomDump (Playground) 10.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 25 20 | 21 | CustomDump (Playground) 11.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 26 27 | 28 | CustomDump (Playground) 2.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 3 34 | 35 | CustomDump (Playground) 3.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 4 41 | 42 | CustomDump (Playground) 4.xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 5 48 | 49 | CustomDump (Playground) 5.xcscheme 50 | 51 | isShown 52 | 53 | orderHint 54 | 6 55 | 56 | CustomDump (Playground) 6.xcscheme 57 | 58 | isShown 59 | 60 | orderHint 61 | 7 62 | 63 | CustomDump (Playground) 7.xcscheme 64 | 65 | isShown 66 | 67 | orderHint 68 | 8 69 | 70 | CustomDump (Playground) 8.xcscheme 71 | 72 | isShown 73 | 74 | orderHint 75 | 9 76 | 77 | CustomDump (Playground) 9.xcscheme 78 | 79 | isShown 80 | 81 | orderHint 82 | 24 83 | 84 | CustomDump (Playground).xcscheme 85 | 86 | isShown 87 | 88 | orderHint 89 | 1 90 | 91 | Modular-TCA.xcscheme_^#shared#^_ 92 | 93 | orderHint 94 | 0 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/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 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/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 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/Login/LoginFeature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginFeature.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import Foundation 9 | import ComposableArchitecture 10 | 11 | struct LoginState: Equatable{ 12 | // var logInResponse: String = "" 13 | } 14 | 15 | enum LoginAction{ 16 | case logIn(Result) 17 | } 18 | 19 | struct LoginEnvironmnet{} 20 | 21 | let loginReducer = Reducer< 22 | LoginState, 23 | LoginAction, 24 | LoginEnvironmnet 25 | >{ state, action, envrionment in 26 | return .none 27 | // switch action{ 28 | // case .onAppear: 29 | // return .none 30 | // case .logIn(.success(let response)): 31 | //// state.logInResponse = response 32 | // return .none 33 | // } 34 | } 35 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/Login/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | struct LoginView: View { 12 | 13 | let store: Store 14 | 15 | var body: some View { 16 | WithViewStore(self.store){ viewStore in 17 | VStack{ 18 | Button { 19 | viewStore.send(.logIn(.success("wimes"))) 20 | } label: { 21 | Text("logIn") 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | //struct LoginView_Previews: PreviewProvider { 29 | // static var previews: some View { 30 | // LoginView() 31 | // } 32 | //} 33 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/Modular_TCAApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modular_TCAApp.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | @main 12 | struct Modular_TCAApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | RootView(store: Store( 16 | initialState: RootState(), 17 | reducer: rootReducer, 18 | environment: RootEnvironment() 19 | )) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/Root/RootFeature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootFeature.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import ComposableArchitecture 9 | 10 | enum RootState: Equatable{ 11 | case login(LoginState) 12 | case tabBar(TabBarState) 13 | 14 | public init() { self = .login(.init())} 15 | } 16 | 17 | 18 | enum RootAction{ 19 | case loginAction(LoginAction) 20 | case tabBarAction(TabBarAction) 21 | } 22 | 23 | struct RootEnvironment{} 24 | 25 | let rootReducer = Reducer< 26 | RootState, 27 | RootAction, 28 | RootEnvironment 29 | >.combine( 30 | loginReducer.pullback( 31 | state: /RootState.login, 32 | action: /RootAction.loginAction, 33 | environment: {_ in LoginEnvironmnet()} 34 | ), 35 | tabBarReducer.pullback( 36 | state: /RootState.tabBar, 37 | action: /RootAction.tabBarAction, 38 | environment: {_ in TabBarEnvironmnet()} 39 | ), 40 | Reducer{ state, action, _ in 41 | switch action { 42 | case .loginAction(.logIn(.success(let response))): 43 | state = .tabBar(.init(loginData: response)) 44 | return .none 45 | case .loginAction: 46 | return .none 47 | case .tabBarAction: 48 | return .none 49 | } 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/Root/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | 11 | struct RootView: View { 12 | let store: Store 13 | 14 | init(store: Store){ 15 | self.store = store 16 | } 17 | 18 | var body: some View { 19 | SwitchStore(self.store){ 20 | CaseLet(state: /RootState.login, action: RootAction.loginAction){ store in 21 | LoginView(store: store) 22 | } 23 | CaseLet(state: /RootState.tabBar, action: RootAction.tabBarAction) { store in 24 | TabBarView(store: store) 25 | } 26 | } 27 | } 28 | } 29 | 30 | //struct RootView_Previews: PreviewProvider { 31 | // static var previews: some View { 32 | // RootView() 33 | // } 34 | //} 35 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/TabBar/TabBarFeature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarFeature.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import ComposableArchitecture 9 | import Effects 10 | import A 11 | import B1 12 | 13 | 14 | struct TabBarState: Equatable{ 15 | var loginData: String 16 | 17 | var a1State = A1State() 18 | var b1State = B1State() 19 | } 20 | 21 | enum TabBarAction{ 22 | case a1Action(A1Action) 23 | case b1Action(B1Action) 24 | } 25 | 26 | struct TabBarEnvironmnet{} 27 | 28 | let tabBarReducer = Reducer< 29 | TabBarState, 30 | TabBarAction, 31 | TabBarEnvironmnet 32 | >.combine( 33 | a1Reducer.pullback( 34 | state: \.a1State, 35 | action: /TabBarAction.a1Action, 36 | environment: { _ in 37 | .init() 38 | }), 39 | b1Reducer.pullback( 40 | state: \.b1State, 41 | action: /TabBarAction.b1Action, 42 | environment: { _ in 43 | .init( 44 | request: {EffectsImpl().numbersApiTwo()}, 45 | mainQueue: {.main} 46 | ) 47 | }), 48 | Reducer{state, action, _ in 49 | switch action{ 50 | case .b1Action(.onAppear): 51 | state.b1State.loginData = state.loginData 52 | return .none 53 | default: 54 | // return Effect(value: .b2Action(.onAppear)) 55 | return .none 56 | } 57 | } 58 | ) 59 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCA/Sources/TabBar/TabBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarView.swift 3 | // Modular-TCA 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import SwiftUI 9 | import ComposableArchitecture 10 | import A 11 | import B1 12 | import B2 13 | 14 | struct TabBarView: View { 15 | let store: Store 16 | 17 | var body: some View { 18 | WithViewStore(self.store){ viewStore in 19 | TabView{ 20 | A1View(store: self.store.scope( 21 | state: \.a1State, 22 | action: TabBarAction.a1Action 23 | )) 24 | .tabItem { 25 | Image(systemName: "list.dash") 26 | Text("A") 27 | } 28 | 29 | NavigationView{ 30 | B1View(store: self.store.scope( 31 | state: \.b1State, 32 | action: TabBarAction.b1Action 33 | )) 34 | } 35 | .tabItem { 36 | Image(systemName: "list.dash") 37 | Text("B") 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCATests/Modular_TCATests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modular_TCATests.swift 3 | // Modular-TCATests 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import XCTest 9 | @testable import Modular_TCA 10 | 11 | class Modular_TCATests: 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 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCAUITests/Modular_TCAUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modular_TCAUITests.swift 3 | // Modular-TCAUITests 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import XCTest 9 | 10 | class Modular_TCAUITests: 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 | -------------------------------------------------------------------------------- /Modular-TCA/Modular-TCAUITests/Modular_TCAUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modular_TCAUITestsLaunchTests.swift 3 | // Modular-TCAUITests 4 | // 5 | // Created by Wimes on 2022/01/08. 6 | // 7 | 8 | import XCTest 9 | 10 | class Modular_TCAUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.assets/app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/README.assets/app.gif -------------------------------------------------------------------------------- /README.assets/image-20220113233726026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/README.assets/image-20220113233726026.png -------------------------------------------------------------------------------- /README.assets/image-20220113233800114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/README.assets/image-20220113233800114.png -------------------------------------------------------------------------------- /README.assets/image-20220113235007940.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/README.assets/image-20220113235007940.png -------------------------------------------------------------------------------- /README.assets/image-20220115003338295.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiryun/Modular-TCA/9fc60a391a445fc33491c99d2f270177ea5c31c1/README.assets/image-20220115003338295.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **해당 repo는 https://github.com/dev-wimes/TCAExample 으로 이동/업데이트 했습니다!** 2 | # 개요 3 | 4 | TCA를 공부하고 나서 내가 느낀 TCA의 단점은 하나이다. Feature에 파편화에 따른 관리의 복잡함. 5 | 6 | 이 단점을 보완하고자 생각한 [아이디어](https://github.com/kiryun/TIL/blob/master/Apple/TCA/TCA_101_after.md#%EC%8B%9C%EB%8F%84%ED%95%B4%EB%B3%BC%EB%A7%8C%ED%95%9C-%EA%B2%83%EB%93%A4)가 있는데 각 Feature의 모듈화이다. 7 | 아래는 해당내용의 PoC(Proof of Concept)이다. 8 | 9 | ### 전체적인 App Feature 구조 및 View Flow 10 | 11 | image-20220113233726026 12 | 13 | * Root 14 | * 모든 Feature들을 통합해서 하나의 App을 만들어준다. 15 | * Login 16 | * Root에서 바로 MainFeature(TabBar)로 넘어가는 앱은 드물다. 보통은 Login 성공 시 MainView를 보여준다. 17 | * 이 경우 Login -> TabBar사이의 데이터 이동은 어떻게 되는지 한번 보도록 한다. 18 | * TabBar 19 | * MainFeature 역할을 한다. 실제 앱의 전반적인 Feature를 갖고 있다. 20 | * A ~ B 21 | * TabBar 아래에 있는 Feature(View)들 22 | 23 | ### Dependency Architecture (with SwiftPM) 24 | 25 | image-20220113233800114 26 | 27 | * [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) 28 | * SwiftPM( .static ) 29 | * Effects 30 | * SwiftPM( .dynamic ) 31 | * Effects는 TCA를 dependency로 갖고 있다. 32 | * Effects에는 외부 dependency를 정의하고 있다. 33 | * A ~ B 34 | * SwiftPM( .static ) 35 | * App의 핵심 Feature들. 각 Feature들은 Effects를 dependency로 갖고 있다. 36 | * Modular-TCA 37 | * .xcodeproj 38 | * 실제 App을 구동시키는 프로젝트. 39 | * TabBar와 Login을 갖고 있다. 40 | * Login은 Login 기능을 담당하고 있으며, 처음 App을 실행하면 가장 먼저 마주하는 기능이다. Login에 성공하면 TabBar로 변경된다. 41 | * TabBar는 App의 핵심 View들을 표출한다. 42 | 43 | ### 이번 프로젝트에서 봐야 할 주요 포인트 44 | 45 | * 모둘화(SPM) + TCA 46 | * TCA에서 View간의 통신 47 | * 같은 레벨에서의 통신 48 | * 서로 다른 레벨에서의 통신 49 | * 상위 -> 하위 50 | * 하위 -> 상위 51 | * Package 간의 데이터 통신 52 | * TCA에서 SceneWindow교체 53 | * Package간의 NavigationLink 54 | 55 | # 구현 56 | 57 | ## Environments 58 | 59 | * Target OS 60 | * iOS 15.2 61 | * Xcode 13.2.1 (13C100) 62 | * MacOS 12.1 63 | * TCA: 0.33.0 64 | 65 | 66 | 67 | ## 어떤 App을 만드는가? 68 | 69 | Login버튼을 누르면 해당 화면이 변경되고 TabBar를 통해 A1View가 나온다. A\~B View는 [numbersapi](http://numbersapi.com/)에서 `/1` \~ `/4` 으로 요청해서 받은 값을 보여준다. 70 | 그 중 B1View는 login할 때 전달 받은 데이터도 같이 표출해준다. A~B는 NavigationView내부에 있다. 71 | 72 | ![Simulator Screen Recording - iPhone 12 mini - 2022-01-15 at 01.24.01](README.assets/app.gif) 73 | 74 | ## SwiftPM을 이용한 모듈 구성 75 | 76 | 1. Repository들을 구현하는 Effects Package부터 구현 77 | 2. 핵심 Feature 단계인 A ~ B를 구현 78 | 3. Root 역할을 하는 프로젝트를 생성해서 package들을 import한다. 79 | 80 | ### Effects 81 | 82 | **Package.swift** 83 | 84 | TCA를 github에서 받는게 아니라 path를 통해서 받고 있다. 85 | 86 | ```swift 87 | .package( 88 | name: "swift-composable-architecture", 89 | path: "../swift-composable-architecture" 90 | ) 91 | ``` 92 | 93 | [여기](https://forums.swift.org/t/how-to-integrate-tca-framework-as-of-0-1-3/36443) 에서 github에서 직접 package를 받는 것이 안된다고 한다. 따라서 직접 폴더에 TCA를 다운받고 path를 입력해줘야 한다고 한다. 94 | 95 | image-20220113235007940 96 | 97 | > 이제는 된다고 한다. 아래처럼 하면 github에서 알아서 받아온다. 98 | > 99 | > ```swift 100 | > package( 101 | > url: "https://github.com/pointfreeco/swift-composable-architecture", 102 | > .upToNextMajor(from: "0.33.0") 103 | > ) 104 | > ``` 105 | 106 | 아래 product 부분을 보면 dynamic으로 되어 있는데 이건 나중에 설명하기로 한다. 107 | 108 | ```swift 109 | .library(name: "Effects", type: .dynamic, targets: ["Effects"]), 110 | ``` 111 | 112 | 113 | 114 | ```swift 115 | import PackageDescription 116 | 117 | let package = Package( 118 | name: "Effects", 119 | platforms: [ 120 | .iOS(.v14) 121 | ], 122 | products: [ 123 | .library(name: "Effects", type: .dynamic, targets: ["Effects"]), 124 | ], 125 | dependencies: [ 126 | 127 | // MARK: https://forums.swift.org/t/how-to-integrate-tca-framework-as-of-0-1-3/36443 여기서는 안된다고 함. 128 | // 위 링크는 옛날 자료고 아래처럼 하면 됨 129 | // package( 130 | // url: "https://github.com/pointfreeco/swift-composable-architecture", 131 | // .upToNextMajor(from: "0.33.0") 132 | // ) 133 | .package( 134 | name: "swift-composable-architecture", 135 | path: "../swift-composable-architecture" 136 | ) 137 | ], 138 | targets: [ 139 | .target( 140 | name: "Effects", 141 | dependencies: [ 142 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 143 | ]), 144 | .testTarget( 145 | name: "EffectsTests", 146 | dependencies: ["Effects"]), 147 | ] 148 | ) 149 | 150 | ``` 151 | 152 | 153 | 154 | **Effects.swift** 155 | 156 | Effects는 단순하다. [numbersapi](http://numbersapi.com) 에서 1 ~ 4 에 해당 하는 api를 요청한다. 1은 A1, 2는 A2, ... 이런식으로 각 Feature에서 사용할 예정이다. 157 | 158 | Combine 의 `dataTaskPublisher`를 통해 통신을 하고 그 값을 `eraseToEffect()` 를 이용해 Wrapping 한다. 159 | 160 | ```swift 161 | import Combine 162 | import ComposableArchitecture 163 | 164 | public enum ApiError: Error{ ... } 165 | 166 | public protocol Effects{ 167 | func numbersApiOne() -> Effect 168 | ... 169 | } 170 | 171 | public class EffectsImpl: Effects{ 172 | public init(){} 173 | public func numbersApiOne() -> Effect{ 174 | guard let url = URL(string: "http://numbersapi.com/1") else{ 175 | fatalError("Error on creating url") 176 | } 177 | 178 | return URLSession.shared.dataTaskPublisher(for: url) 179 | .mapError{_ in ApiError.downloadError} 180 | .map(\.data) 181 | .compactMap{ String(data: $0, encoding: .utf8)} 182 | .eraseToEffect() 183 | } 184 | 185 | ... 186 | } 187 | ``` 188 | 189 | 190 | 191 | ### A 192 | 193 | A package 의 경우에는 하나의 package 내에서 2개 이상의 Feature를 포함하는 예제를 보여주기 위해 만들었다. 포함하고 있는 Feature는 A1, A2이다. 194 | 195 | **Package.swift** 196 | 197 | 집중해서 봐야할 부분은 products에서 library가 `.dynamic`이 아니다.(Effects의 경우에는 `.dynamic`이다. ) A Package는 SPM이 자동으로 선택하고 있다. 198 | 199 | 그렇다면 왜 Effects는 library type이 `.dynamic` 이고 A는 그렇지 않을까? 만약 Effects가 `.static`이라고 가정했을 때를 보자 200 | A와 B가 Effects를 사용한다고 하면 A와 B의 바이너리에 Effects의 의존 Package가 복사되어 들어가려고 하므로, A와 B 바이너리 내에 중복해서 존재하게 된다. 따라서 이 경우, 컴파일러가 중복된다고 판단하고 컴파일 에러를 발생시킨다. 201 | 202 | > Reference 203 | > 204 | > * https://minsone.github.io/ios/mac/swift-package-manager-proxy-modular 205 | > * https://zeddios.tistory.com/1313 206 | > * https://github.com/kiryun/TIL/blob/master/Apple/Framework/DynamicFramework_StaticFramework_StaticLibrary.md 207 | 208 | ```swift 209 | import PackageDescription 210 | 211 | let package = Package( 212 | name: "A", 213 | platforms: [.iOS(.v14)], 214 | products: [.library(name: "A", targets: ["A"])], 215 | dependencies: [.package(name: "Effects", path: "../Effects")], 216 | targets: [ 217 | .target(name: "A", dependencies: ["Effects"]), 218 | .testTarget(name: "ATests", dependencies: ["A"]), 219 | ] 220 | ) 221 | ``` 222 | 223 | **A1Feature.swift** 224 | 225 | 먼저 Environment를 보자. request와 queue가 Environment 내부에서 정의하고 있다. 의존성 주입이 없다. 226 | 사실 A1Feature의 해당 부분은 좋지 못한 방법이다. environment는 항상 Effects의 `nubmersApiOne()` 를 요청하고 있고, 항상 mainQueue에서 동작한다. 227 | 사실 live 에서만 사용한다면 문제될게 없다. 그러나 test를 위해 mock data를 사용하려고하면 문제가 된다. 외부에서 test 환경에 대한 데이터를 받을 수 없기 때문이다. 228 | 이러면 A1Feature 내부에 test용 mock data를 만들어 줘야 한다. 229 | 나중에 BFeature를 만들면서 어떠한 차이가 있는지 보도록 한다. 230 | 231 | 그리고 봐야하는 부분이 A1Action, A1State 인데 232 | Modular-TCA App은 Root->TabBar->A1->A2 순으로 View가 이동한다. "Composable" Architecture라는 이름에 맞게 Root는 TabBar를 갖고 있고, TabBar는 나머지 Feature들(A~B)을 갖고 있다. 233 | A1과 A2를 각각 동일한 level로 볼지 아니면 A1하위에 A2가 있다고 볼지 차이가 있겠지만 현재 A1, A2는 같은 level 상으로 두고 있고, Root, TabBar, A1에서 A2의 데이터에 직접적으로 접근할 수 있는 방법이 없다.(왜냐면 데이터의 흐름에서 접근할 수 있는 부분은 State와 Action인데 현재 A1State, A1Action에는 A2에 대한 내용이 없다.) 234 | 어떤게 더 나은 방법인지는 그때 그때 마다 다를듯하지만, 개인적으로는 Root에서 모든 Feature에 접근이 가능해야 하지 않나 싶다. 235 | 236 | ```swift 237 | import Effects 238 | import ComposableArchitecture 239 | 240 | public struct A1State: Equatable{ 241 | public init(){} 242 | var resultString: String = "" 243 | } 244 | 245 | public enum A1Action: Equatable{ 246 | case onAppear 247 | case dataLoaded(Result) 248 | } 249 | 250 | public struct A1Environment{ 251 | var request: () -> Effect = { 252 | let effects: Effects = EffectsImpl() 253 | return effects.numbersApiOne() 254 | } 255 | var mainQueue: () -> AnySchedulerOf = {.main} 256 | public init(){} 257 | } 258 | 259 | public let a1Reducer = Reducer< 260 | A1State, 261 | A1Action, 262 | A1Environment 263 | >{ state, action, environment in 264 | switch action{ 265 | case .onAppear: 266 | return environment.request() 267 | .receive(on: environment.mainQueue()) 268 | .catchToEffect() 269 | .map(A1Action.dataLoaded) 270 | case .dataLoaded(let result): 271 | switch result{ 272 | case .success(let result): 273 | state.resultString = result 274 | case .failure(let error): 275 | break 276 | } 277 | return .none 278 | } 279 | } 280 | 281 | func dummyA1Effect() -> Effect{ 282 | let dummyString = "test" 283 | return Effect(value: dummyString) 284 | } 285 | ``` 286 | 287 | **A1View.swift** 288 | 289 | A1View에는 딱히 뭐가 없다. NavigiatonView로 A1View를 감쌌고, Effects에서 받아온 값(viewStore.resultString)을 Text로 화면에 보여주고 있다. 290 | 그리고 NavigationLink를 이용해 Text 버튼을 누를 경우 A2View로 넘어간다. 따로 A1이 A2에 대한 정보를 갖고 있지 않기 때문에 바로 Store를 생성해서 호출한다. 291 | 292 | ```swift 293 | import SwiftUI 294 | import ComposableArchitecture 295 | import Effects 296 | 297 | public struct A1View: View { 298 | let store: Store 299 | 300 | public init(store: Store){ 301 | self.store = store 302 | } 303 | 304 | public var body: some View { 305 | WithViewStore(self.store){ viewStore in 306 | NavigationView{ 307 | VStack{ 308 | Text(viewStore.resultString) 309 | NavigationLink { 310 | A2View(store: Store( 311 | initialState: A2State(resultString: ""), 312 | reducer: a2Reducer, 313 | environment: A2Environment( 314 | request: {EffectsImpl().numbersApiThree()}, 315 | mainQueue: {.main} 316 | ))) 317 | } label: { 318 | Text("open the A2 View") 319 | } 320 | } 321 | .navigationTitle("A1") 322 | } 323 | .onAppear { 324 | viewStore.send(.onAppear) 325 | } 326 | } 327 | } 328 | } 329 | ``` 330 | 331 | 332 | 333 | ### B1 ~ B2 334 | 335 | B1 **Package.swift** 336 | 337 | 볼건 별로 없고 Effects를 의존성으로 갖고 있고, B2도 사용하기 때문에 B2 도 의존성에 넣었다. 338 | 339 | ```swift 340 | import PackageDescription 341 | 342 | let package = Package( 343 | name: "B1", 344 | platforms: [.iOS(.v14)], 345 | products: [ 346 | .library(name: "B1", targets: ["B1"]) 347 | ], 348 | dependencies: [ 349 | .package(name: "Effects", path: "../Effects"), 350 | .package(name: "B2", path: "../B2") 351 | ], 352 | targets: [ 353 | .target( 354 | name: "B1", 355 | dependencies: ["Effects", "B2"]), 356 | .testTarget( 357 | name: "B1Tests", 358 | dependencies: ["B1"]), 359 | ] 360 | ) 361 | ``` 362 | 363 | **B1Feature.swift** 364 | 365 | ```swift 366 | import Effects 367 | import ComposableArchitecture 368 | import B2 369 | 370 | public struct B1State: Equatable{ 371 | public var loginData: String = "" 372 | public var resultString: String = "" 373 | // 여기서 B2State를 생성해서 넣어주고 있는데 외부에서 생성해서 넣어도 된다. 다만 지금 상황에서는 어울리지 않는거 같아서 여기서 생성 374 | public var b2State = B2State(resultString: "") 375 | // 외부로 접근이 제한된 변수가 필요하다면 private으로 선언 376 | // private internalData: String = "" 377 | public init(){} 378 | } 379 | 380 | public enum B1Action{ 381 | case onAppear 382 | case dataLoaded(Result) 383 | case b2Action(B2Action) 384 | } 385 | 386 | public struct B1Environment{ 387 | var request: () -> Effect 388 | var mainQueue: () -> AnySchedulerOf 389 | 390 | // 의존성 주입을 통해 request, mainQueue를 설정 391 | public init( 392 | request: @escaping () -> Effect, 393 | mainQueue: @escaping () -> AnySchedulerOf 394 | ){ 395 | self.request = request 396 | self.mainQueue = mainQueue 397 | } 398 | } 399 | 400 | public let b1Reducer = Reducer< 401 | B1State, 402 | B1Action, 403 | B1Environment 404 | // combine을 써서 reducer를 합친다. 405 | >.combine( 406 | // b2Reducer도 b1Reducer를 통해 접근이 가능하도록 한다. 407 | b2Reducer.pullback( 408 | state: \.b2State, 409 | action: /B1Action.b2Action, 410 | environment: { _ in 411 | // 여기서 B2 Environment에 request, mainQueue를 주입해준다. 412 | .init( 413 | request: EffectsImpl().numbersApiFour, 414 | mainQueue: {.main}) 415 | }), 416 | // b1에 대한 reducer는 따로 빼준다. 417 | Reducer{ state, action, environment in 418 | switch action{ 419 | case .onAppear: 420 | return environment.request() 421 | .receive(on: environment.mainQueue()) 422 | .catchToEffect(B1Action.dataLoaded) 423 | case .dataLoaded(.success(let result)): 424 | state.resultString = result 425 | return .none 426 | case .dataLoaded(.failure(let error)): 427 | return .none 428 | default: 429 | return .none 430 | } 431 | } 432 | ) 433 | ``` 434 | 435 | **B1View.swift** 436 | 437 | 이전에 봤던 A1 -> A2 와는 다르게 B1에서 B2를 호출하는 방식은 `scope`를 이용해서 438 | 439 | ```swift 440 | import SwiftUI 441 | import ComposableArchitecture 442 | import B2 443 | 444 | public struct B1View: View { 445 | 446 | let store: Store 447 | 448 | public init(store: Store){ 449 | self.store = store 450 | } 451 | 452 | public var body: some View { 453 | WithViewStore(self.store){ viewStore in 454 | VStack{ 455 | Text("login Data: "+viewStore.loginData) 456 | Text(viewStore.resultString) 457 | NavigationLink { 458 | // A package에서 A2를 호출할 때와는 다르게 scope를 사용해서 B2View의 Store를 주입해준다. 459 | // B1에서 정의한 B2State, B2Action을 주입한다. 460 | // B2에서 직접만든 state, action 이 아닌 B1에서 갖고 있는 state, action을 넣고 있음. 461 | B2View(store: self.store.scope( 462 | state: \.b2State, 463 | action: B1Action.b2Action 464 | )) 465 | } label: { 466 | Text("Open the B2View") 467 | } 468 | } 469 | .navigationTitle("B1") 470 | .onAppear { 471 | viewStore.send(.onAppear) 472 | } 473 | } 474 | } 475 | } 476 | ``` 477 | 478 | 479 | 480 | ### Modular-TCA.xcodeproject 481 | 482 | 이번엔 package가 아닌 `@main`을 갖고 있는 App을 구현할 차례이다. 483 | 484 | Root가 TabBar, Login을 갖고 있고, 상황에 따라서 Login에서 TabBar로 SceneWindow가 TabBar로 변경된다. TabBar안에 A1, B1이 있다. 485 | 486 | image-20220115003338295 487 | 488 | **RootFeature.swift** 489 | 490 | State는 login/tabBar에 따라서 View가 변해야 하므로 enum으로 login, tabBar를 만들었고, 각각 State를 연관값으로 갖고 있다. 491 | 그리고 처음 View는 login으로 설정한다. 해당 내용은 [tic tac toe](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/TicTacToe)에서 아이디어를 얻었다. 492 | 493 | Action도 다른 Feature들과 마찬가지로 하위 Feature에 대한 Action을 갖고 있다. 494 | 495 | Reducer도 또한 하위 reducer에 대한 동작을 캐치하기 위해 pullback을 사용한다. 이러면 Login, TabBar 에 대한 동작(reducer)를 Root의 State, Action에서 받을 수 있다. 496 | (pullback은 간단히 말해서 하위 State, Action, Environment를 상위 State, Action, Environment에서 작동할 수 있도록 해주는거다. 그래서 하위 모듈에 대한 reducer를 pullback을 통해 상위 State, Action에 매핑하고 있다.) 497 | 마지막으로 Root자체의 reducer를 갖고 있다. 498 | 499 | reducer에서 가장 주의깊게 봐야할 점은 LoginAction을 RootFeature에서 정의하고 있다는 건데 RootFeature는 Login, TabBar 둘다 동시에 접근이 가능하다. 500 | 즉, login 후 넘겨줘야 하는 data의 이동은 RootFeature에서 담당한다. 501 | 502 | LoginFeature에서 발생하는 Action을 RootFeature의 reducer에서 받아서 처리하도록 만들었다. 503 | logIn success action이 들어오면 state는 `.tabBar`로 변경해주고 loginData를 받아와서 TabBar Feature로 넘겨준다. 504 | 505 | 이처럼 combine으로 하위 모듈을 구성할 수 도 있고, 하위 모듈에 대한 action을 받아서 처리할 수 도 있다. 506 | 507 | ```swift 508 | import ComposableArchitecture 509 | 510 | enum RootState: Equatable{ 511 | case login(LoginState) 512 | case tabBar(TabBarState) 513 | 514 | public init() { self = .login(.init())} 515 | } 516 | 517 | enum RootAction{ 518 | case loginAction(LoginAction) 519 | case tabBarAction(TabBarAction) 520 | } 521 | 522 | struct RootEnvironment{} 523 | 524 | let rootReducer = Reducer< 525 | RootState, 526 | RootAction, 527 | RootEnvironment 528 | >.combine( 529 | loginReducer.pullback( 530 | state: /RootState.login, 531 | action: /RootAction.loginAction, 532 | environment: {_ in LoginEnvironmnet()} 533 | ), 534 | tabBarReducer.pullback( 535 | state: /RootState.tabBar, 536 | action: /RootAction.tabBarAction, 537 | environment: {_ in TabBarEnvironmnet()} 538 | ), 539 | Reducer{ state, action, _ in 540 | switch action { 541 | // logIn success action이 들어오면 state는 `.tabBar`로 변경해주고 loginData를 받아와서 TabBar Feature로 넘겨준다. 542 | case .loginAction(.logIn(.success(let response))): 543 | state = .tabBar(.init(loginData: response)) 544 | return .none 545 | case .loginAction: 546 | return .none 547 | case .tabBarAction: 548 | return .none 549 | } 550 | } 551 | ) 552 | ``` 553 | 554 | **RootView.swift** 555 | 556 | State가 case로 되어 있기 때문에 `SwitchStore`를 사용하고 `CaseLet`을 이용한다. Feture에서 enum 으로 정의한 state에 따라서 View가 바뀐다. 557 | 558 | ```swift 559 | import SwiftUI 560 | import ComposableArchitecture 561 | 562 | struct RootView: View { 563 | let store: Store 564 | 565 | init(store: Store){ 566 | self.store = store 567 | } 568 | 569 | var body: some View { 570 | SwitchStore(self.store){ 571 | CaseLet(state: /RootState.login, action: RootAction.loginAction){ store in 572 | LoginView(store: store) 573 | } 574 | CaseLet(state: /RootState.tabBar, action: RootAction.tabBarAction) { store in 575 | TabBarView(store: store) 576 | } 577 | } 578 | } 579 | } 580 | ``` 581 | 582 | **LoginFeature.swift** 583 | 584 | Login쪽에서는 Action만 보면 된다. `logIn`의 성공 여부에 따라 동작하도록 `Result`를 넣었다. (사실 아래 코드는 실패할 일이 없다.) 585 | 586 | 그리고 reducer에서는 Login에 대한 모든 처리는 rootReducer에서 담당하고 있기 때문에 `return .none`만 넣어준다. 587 | 588 | ```swift 589 | import Foundation 590 | import ComposableArchitecture 591 | 592 | struct LoginState: Equatable{} 593 | 594 | enum LoginAction{ 595 | case logIn(Result) 596 | } 597 | 598 | struct LoginEnvironmnet{} 599 | 600 | let loginReducer = Reducer< 601 | LoginState, 602 | LoginAction, 603 | LoginEnvironmnet 604 | >{ state, action, envrionment in return .none} 605 | ``` 606 | 607 | **LoginView.swift** 608 | 609 | ```swift 610 | import SwiftUI 611 | import ComposableArchitecture 612 | 613 | struct LoginView: View { 614 | 615 | let store: Store 616 | 617 | var body: some View { 618 | WithViewStore(self.store){ viewStore in 619 | VStack{ 620 | // login 버튼을 누르면 login success action과 함께 "wimes"라는 값을 전달한다. 621 | Button { 622 | viewStore.send(.logIn(.success("wimes"))) 623 | } label: { 624 | Text("logIn") 625 | } 626 | } 627 | } 628 | } 629 | } 630 | ``` 631 | 632 | **TabBarFeature.swift** 633 | 634 | TabBar는 Login에서 `loginData` 를 받는다.(정확히는 Login에서 action을 발생시키고 Root에서 데이터를 준다.) 635 | 그렇기 때문에 state에 해당 프로퍼티를 선언한다. 636 | 637 | 그리고 A1, B1에 대한 state, action를 선언해준다. 638 | 639 | 그리고 reducer에서 각 Feature에 대한 reducer를 pullback을 통해 매핑하고 640 | B1에서 `.onAppear`이 발생하면 B1의 loginData에 현재 `TabBarState`의 `loginData`를 넣어준다. 641 | 642 | 만약 B2에 대한 action과 state를 핸들링하고 싶다면 State와 Action를 추가하고 Reducer에 pullback을 사용해 각 state, action을 매핑해주면 된다. 643 | 644 | 여기서 pullback에 대해 다시한번 간단히 설명하자면 하위 Feature에 대한 State, Action을 매핑한다. 그리고 매핑된 State, Action(`TabBarState.a1State`, `TabBarState.b1State`, `TabBarAction.a1Action`, `TabBarAction.b1Action)을 이용해 동작을 정의한다. 645 | 646 | ```swift 647 | import ComposableArchitecture 648 | import Effects 649 | import A 650 | import B1 651 | 652 | struct TabBarState: Equatable{ 653 | var loginData: String 654 | var a1State = A1State() 655 | var b1State = B1State() 656 | } 657 | 658 | enum TabBarAction{ 659 | case a1Action(A1Action) 660 | case b1Action(B1Action) 661 | } 662 | 663 | struct TabBarEnvironmnet{} 664 | 665 | let tabBarReducer = Reducer< 666 | TabBarState, 667 | TabBarAction, 668 | TabBarEnvironmnet 669 | >.combine( 670 | a1Reducer.pullback( 671 | state: \.a1State, 672 | action: /TabBarAction.a1Action, 673 | environment: { _ in 674 | .init() 675 | }), 676 | b1Reducer.pullback( 677 | state: \.b1State, 678 | action: /TabBarAction.b1Action, 679 | environment: { _ in 680 | .init( 681 | request: {EffectsImpl().numbersApiTwo()}, 682 | mainQueue: {.main} 683 | ) 684 | }), 685 | Reducer{state, action, _ in 686 | switch action{ 687 | case .b1Action(.onAppear): 688 | state.b1State.loginData = state.loginData 689 | return .none 690 | default: 691 | return .none 692 | } 693 | } 694 | ) 695 | ``` 696 | 697 | **TabBarView.swift** 698 | 699 | > A1에는 NavigationView가 없고 B1에는 있는데 신경 안써도 된다. 뻘짓의 흔적이다. 700 | 701 | ```swift 702 | import SwiftUI 703 | import ComposableArchitecture 704 | import A 705 | import B1 706 | import B2 707 | 708 | struct TabBarView: View { 709 | let store: Store 710 | 711 | var body: some View { 712 | WithViewStore(self.store){ viewStore in 713 | TabView{ 714 | A1View(store: self.store.scope( 715 | state: \.a1State, 716 | action: TabBarAction.a1Action 717 | )) 718 | .tabItem { 719 | Image(systemName: "list.dash") 720 | Text("A") 721 | } 722 | NavigationView{ 723 | B1View(store: self.store.scope( 724 | state: \.b1State, 725 | action: TabBarAction.b1Action 726 | )) 727 | } 728 | .tabItem { 729 | Image(systemName: "list.dash") 730 | Text("B") 731 | } 732 | } 733 | } 734 | } 735 | } 736 | ``` 737 | 738 | # 느낀점 739 | 740 | 모듈화에서 나올 수 있는 [circular dependency](http://minsone.github.io/programming/swift-solved-circular-dependency-from-dependency-injection-container) 확률이 현저히 낮아진다. 741 | 상위 Feature에서 action을 발생시키고, action에 대한 처리가 가능하기 때문. 742 | b1(또는 a1 뭐가 됐든 하위 Feature)에서 b2에 대한 action을 핸들링하거나 state에 접근한다고 하면 Action, State, Reducer를 정의해주면 된다. 743 | 744 | composable architecture라는 이름과 걸맞게 모든 feature를 모듈로 빼기 쉬었고, 접근 제어 또한 매우 쉬었다. 745 | 746 | 747 | 748 | --------------------------------------------------------------------------------