├── 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 |
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 |
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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------