├── .gitignore
├── Example
├── Example
│ ├── Configs
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Example.entitlements
│ │ └── Info.plist
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── ExampleApp.swift
│ ├── URL+Extensions.swift
│ ├── Dependencies.swift
│ ├── Scene
│ │ ├── Third
│ │ │ ├── ThirdCore.swift
│ │ │ └── ThirdView.swift
│ │ ├── Root
│ │ │ ├── RootView.swift
│ │ │ └── RootCore.swift
│ │ ├── First
│ │ │ ├── FirstView.swift
│ │ │ └── FirstCore.swift
│ │ └── Second
│ │ │ ├── SecondView.swift
│ │ │ └── SecondCore.swift
│ ├── Router
│ │ ├── RouterView.swift
│ │ └── RouterCore.swift
│ └── ContentView.swift
└── Example.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── Sources
└── TCARouteStack
│ ├── Exported.swift
│ ├── Protocol
│ ├── RouterState.swift
│ └── RouterAction.swift
│ ├── SwiftUI
│ └── RouteStackStore.swift
│ └── Reducers
│ └── ForEachReducer.swift
├── Package.swift
├── LICENSE
├── Package.resolved
├── README_EN.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/
7 | .netrc
8 |
--------------------------------------------------------------------------------
/Example/Example/Configs/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/TCARouteStack/Exported.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | @_exported import ComposableArchitecture
9 | @_exported import RouteStack
10 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example/Configs/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/TCARouteStack/Protocol/RouterState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouterState.swift
3 | //
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import ComposableArchitecture
9 | import RouteStack
10 |
11 | public protocol RouterState: Equatable {
12 | associatedtype Screen: Equatable
13 |
14 | var paths: [RoutePath] { get set }
15 | }
16 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleApp.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ExampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | RouterView(store: .init(initialState: .init(), reducer: { Router() }))
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Example/Example/Configs/Example.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example/URL+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+Extensions.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | public func valueOf(_ name: String) -> String? {
12 | guard let url = URLComponents(string: self.absoluteString) else { return nil }
13 | return url.queryItems?.first(where: { $0.name == name })?.value
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Example/Example/Dependencies.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dependencies.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import UIKit
9 | import ComposableArchitecture
10 |
11 | extension UIApplication: DependencyKey {
12 | static public var liveValue = UIApplication.shared
13 | }
14 |
15 | extension DependencyValues {
16 | public var uiApplication: UIApplication {
17 | get { self[UIApplication.self] }
18 | set { self[UIApplication.self] = newValue }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/TCARouteStack/Protocol/RouterAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouterAction.swift
3 | //
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import ComposableArchitecture
9 | import RouteStack
10 |
11 | public protocol RouterAction: Equatable {
12 | associatedtype Screen: Equatable
13 | associatedtype ScreenAction: Equatable
14 |
15 | static func updatePaths(_ paths: [RoutePath]) -> Self
16 | static func pathAction(_ id: RoutePath.ID, action: ScreenAction) -> Self
17 | }
18 |
--------------------------------------------------------------------------------
/Example/Example/Configs/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLName
11 | tcaRouteStackExample.com
12 | CFBundleURLSchemes
13 |
14 | tcaRouteStackExample
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TCARouteStack",
7 | platforms: [
8 | .iOS(.v16)
9 | ],
10 | products: [
11 | .library(
12 | name: "TCARouteStack",
13 | targets: ["TCARouteStack"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/Monsteel/RouteStack.git", .upToNextMinor(from: "0.1.2")),
17 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", "1.0.0" ..< "1.7.0"),
18 | ],
19 | targets: [
20 | .target(
21 | name: "TCARouteStack",
22 | dependencies: [
23 | .product(name: "RouteStack", package: "RouteStack"),
24 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
25 | ]
26 | ),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Third/ThirdCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThirdCore.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import ComposableArchitecture
10 | import Foundation
11 |
12 | public struct Third: Reducer {
13 | public struct State: Equatable {
14 | public var value: String
15 |
16 | public init(value: String) {
17 | self.value = value
18 | }
19 | }
20 |
21 | public enum Action: Equatable {
22 | case tappedBackButton
23 | case tappedBackToRootButton
24 | }
25 |
26 | @Dependency(\.uiApplication) var uiApplication
27 |
28 | public var body: some Reducer {
29 | Reduce { state, action in
30 | switch action {
31 | case .tappedBackButton:
32 | uiApplication.open(URL(string: "tcaRouteStackExample://back")!)
33 | return .none
34 | case .tappedBackToRootButton:
35 | uiApplication.open(URL(string: "tcaRouteStackExample://backToRoot")!)
36 | return .none
37 | }
38 | }
39 | }
40 |
41 | public init() { }
42 | }
43 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Third/ThirdView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThirdView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import ComposableArchitecture
11 |
12 | public struct ThirdView: View {
13 | @ObservedObject
14 | private var viewStore: ViewStoreOf
15 | private let store: StoreOf
16 |
17 | public init(store: StoreOf) {
18 | self.viewStore = ViewStoreOf(store, observe: { $0 })
19 | self.store = store
20 | }
21 |
22 | public var body: some View {
23 | VStack(alignment: .leading) {
24 | Text("Third View 입니다.")
25 | .font(.title)
26 |
27 | Text("\(viewStore.value) 로 열렸습니다.")
28 | .font(.subheadline)
29 |
30 | Button("back") {
31 | viewStore.send(.tappedBackButton)
32 | }
33 |
34 | Button("back To root") {
35 | viewStore.send(.tappedBackToRootButton)
36 | }
37 | }
38 | .frame(maxWidth: .infinity, maxHeight: .infinity)
39 | .background(Color.green)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 이영은(Tony)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Root/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import ComposableArchitecture
11 |
12 | public struct RootView: View {
13 | @ObservedObject
14 | private var viewStore: ViewStoreOf
15 | private let store: StoreOf
16 |
17 | public init(store: StoreOf) {
18 | self.viewStore = ViewStoreOf(store, observe: { $0 })
19 | self.store = store
20 | }
21 |
22 | public var body: some View {
23 | VStack(alignment: .leading, spacing: 24) {
24 | Text("Root View 입니다.")
25 | .font(.title)
26 | .foregroundStyle(.red)
27 |
28 | Button("push") {
29 | viewStore.send(.tappedPushButton)
30 | }
31 |
32 | Button("custom-sheet") {
33 | viewStore.send(.tappedCustomSheetButton)
34 | }
35 |
36 | Button("normal-sheet") {
37 | viewStore.send(.tappedNormalSheetButton)
38 | }
39 |
40 | Button("cover") {
41 | viewStore.send(.tappedCoverSheetButton)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Example/Example/Configs/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Example/Example/Router/RouterView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouterView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import ComposableArchitecture
11 | import TCARouteStack
12 |
13 | public struct RouterView: View {
14 | @ObservedObject
15 | private var viewStore: ViewStoreOf
16 | private let store: StoreOf
17 |
18 | public init(store: StoreOf) {
19 | self.viewStore = ViewStoreOf(store, observe: { $0 })
20 | self.store = store
21 | }
22 |
23 | private func root() -> some View {
24 | RootView(store: store.scope(state: \.root, action: Router.Action.root))
25 | }
26 |
27 | public var body: some View {
28 | RouteStackStore(store, root: root) { store in
29 | SwitchStore(store) { state in
30 | switch state {
31 | case .first:
32 | CaseLet(/Screen.State.first, action: Screen.Action.first, then: FirstView.init)
33 |
34 | case .second:
35 | CaseLet(/Screen.State.second, action: Screen.Action.second, then: SecondView.init)
36 |
37 | case .third:
38 | CaseLet(/Screen.State.third, action: Screen.Action.third, then: ThirdView.init)
39 | }
40 | }
41 | }
42 | .onOpenURL { url in
43 | viewStore.send(.openURL(url))
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Example/Example/Scene/First/FirstView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import ComposableArchitecture
11 |
12 | public struct FirstView: View {
13 | @ObservedObject
14 | private var viewStore: ViewStoreOf
15 | private let store: StoreOf
16 |
17 | public init(store: StoreOf) {
18 | self.viewStore = ViewStoreOf(store, observe: { $0 })
19 | self.store = store
20 | }
21 |
22 | public var body: some View {
23 | VStack(alignment: .leading) {
24 | Text("First View 입니다.")
25 | .font(.title)
26 |
27 | Text("\(viewStore.value) 로 열렸습니다.")
28 | .font(.subheadline)
29 |
30 | Button("back") {
31 | viewStore.send(.tappedBackButton)
32 | }
33 |
34 | Button("push") {
35 | viewStore.send(.tappedPushButton)
36 | }
37 |
38 | Button("custom-sheet") {
39 | viewStore.send(.tappedCustomSheetButton)
40 | }
41 |
42 | Button("normal-sheet") {
43 | viewStore.send(.tappedNormalSheetButton)
44 | }
45 |
46 | Button("cover") {
47 | viewStore.send(.tappedCoverSheetButton)
48 | }
49 | }
50 | .frame(maxWidth: .infinity, maxHeight: .infinity)
51 | .background(Color.orange)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Second/SecondView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecondView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import ComposableArchitecture
11 |
12 | public struct SecondView: View {
13 | @ObservedObject
14 | private var viewStore: ViewStoreOf
15 | private let store: StoreOf
16 |
17 | public init(store: StoreOf) {
18 | self.viewStore = ViewStoreOf(store, observe: { $0 })
19 | self.store = store
20 | }
21 |
22 | public var body: some View {
23 | VStack(alignment: .leading) {
24 | Text("Second View 입니다.")
25 | .font(.title)
26 |
27 | Text("\(viewStore.value) 로 열렸습니다.")
28 | .font(.subheadline)
29 |
30 | Button("back") {
31 | viewStore.send(.tappedBackButton)
32 | }
33 |
34 | Button("push") {
35 | viewStore.send(.tappedPushButton)
36 | }
37 |
38 | Button("custom-sheet") {
39 | viewStore.send(.tappedCustomSheetButton)
40 | }
41 |
42 | Button("normal-sheet") {
43 | viewStore.send(.tappedNormalSheetButton)
44 | }
45 |
46 | Button("cover") {
47 | viewStore.send(.tappedCoverSheetButton)
48 | }
49 | }
50 | .frame(maxWidth: .infinity, maxHeight: .infinity)
51 | .background(Color.orange)
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Root/RootCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootCore.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import ComposableArchitecture
10 | import Foundation
11 |
12 | public struct Root: Reducer {
13 | public struct State: Equatable {
14 |
15 | public init() { }
16 | }
17 |
18 | public enum Action: Equatable {
19 | case tappedPushButton
20 | case tappedCustomSheetButton
21 | case tappedNormalSheetButton
22 | case tappedCoverSheetButton
23 | }
24 |
25 | @Dependency(\.uiApplication) var uiApplication
26 |
27 | public var body: some Reducer {
28 | Reduce { state, action in
29 | switch action {
30 | case .tappedPushButton:
31 | uiApplication.open(URL(string: "tcaRouteStackExample://firstView?value=push")!)
32 | return .none
33 | case .tappedCustomSheetButton:
34 | uiApplication.open(URL(string: "tcaRouteStackExample://firstView?value=customSheet")!)
35 | return .none
36 | case .tappedNormalSheetButton:
37 | uiApplication.open(URL(string: "tcaRouteStackExample://firstView?value=normalSheet")!)
38 | return .none
39 | case .tappedCoverSheetButton:
40 | uiApplication.open(URL(string: "tcaRouteStackExample://firstView?value=cover")!)
41 | return .none
42 | }
43 | }
44 | }
45 |
46 | public init() { }
47 | }
48 |
--------------------------------------------------------------------------------
/Example/Example/Scene/First/FirstCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstCore.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import ComposableArchitecture
10 | import Foundation
11 |
12 | public struct First: Reducer {
13 | public struct State: Equatable {
14 | public var value: String
15 |
16 | public init(value: String) {
17 | self.value = value
18 | }
19 | }
20 |
21 | public enum Action: Equatable {
22 | case tappedBackButton
23 | case tappedPushButton
24 | case tappedCustomSheetButton
25 | case tappedNormalSheetButton
26 | case tappedCoverSheetButton
27 | }
28 |
29 | @Dependency(\.uiApplication) var uiApplication
30 |
31 | public var body: some Reducer {
32 | Reduce { state, action in
33 | switch action {
34 | case .tappedBackButton:
35 | uiApplication.open(URL(string: "tcaRouteStackExample://back")!)
36 | return .none
37 | case .tappedPushButton:
38 | uiApplication.open(URL(string: "tcaRouteStackExample://secondView?value=push")!)
39 | return .none
40 | case .tappedCustomSheetButton:
41 | uiApplication.open(URL(string: "tcaRouteStackExample://secondView?value=customSheet")!)
42 | return .none
43 | case .tappedNormalSheetButton:
44 | uiApplication.open(URL(string: "tcaRouteStackExample://secondView?value=normalSheet")!)
45 | return .none
46 | case .tappedCoverSheetButton:
47 | uiApplication.open(URL(string: "tcaRouteStackExample://secondView?value=cover")!)
48 | return .none
49 | }
50 | }
51 | }
52 |
53 | public init() { }
54 | }
55 |
--------------------------------------------------------------------------------
/Example/Example/Scene/Second/SecondCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecondCore.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import ComposableArchitecture
10 | import Foundation
11 |
12 | public struct Second: Reducer {
13 | public struct State: Equatable {
14 | public var value: String
15 |
16 | public init(value: String) {
17 | self.value = value
18 | }
19 | }
20 |
21 | public enum Action: Equatable {
22 | case tappedBackButton
23 | case tappedPushButton
24 | case tappedCustomSheetButton
25 | case tappedNormalSheetButton
26 | case tappedCoverSheetButton
27 | }
28 |
29 | @Dependency(\.uiApplication) var uiApplication
30 |
31 | public var body: some Reducer {
32 | Reduce { state, action in
33 | switch action {
34 | case .tappedBackButton:
35 | uiApplication.open(URL(string: "tcaRouteStackExample://back")!)
36 | return .none
37 | case .tappedPushButton:
38 | uiApplication.open(URL(string: "tcaRouteStackExample://thirdView?value=push")!)
39 | return .none
40 | case .tappedCustomSheetButton:
41 | uiApplication.open(URL(string: "tcaRouteStackExample://thirdView?value=customSheet")!)
42 | return .none
43 | case .tappedNormalSheetButton:
44 | uiApplication.open(URL(string: "tcaRouteStackExample://thirdView?value=normalSheet")!)
45 | return .none
46 | case .tappedCoverSheetButton:
47 | uiApplication.open(URL(string: "tcaRouteStackExample://thirdView?value=cover")!)
48 | return .none
49 | }
50 | }
51 | }
52 |
53 | public init() { }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/TCARouteStack/SwiftUI/RouteStackStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouteStackStore.swift
3 | //
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import ComposableArchitecture
9 | import RouteStack
10 | import Foundation
11 | import SwiftUI
12 |
13 | public struct RouteStackStore<
14 | State: RouterState,
15 | Action: RouterAction,
16 | Screen: Equatable,
17 | Root: View,
18 | Destination: View
19 | >: View
20 | where
21 | State.Screen == Action.Screen,
22 | State.Screen == Screen
23 | {
24 | let store: Store
25 | @ViewBuilder let root: () -> Root
26 | @ViewBuilder let destination: (Store) -> Destination
27 |
28 | func scopedStore(id: RoutePath.ID, screen: Screen) -> Store {
29 | var screen = screen
30 | return store.scope(
31 | state: {
32 | if let index = $0.paths.firstIndex(where: { $0.id == id }) {
33 | screen = $0.paths[safe: index]?.data ?? screen
34 | }
35 | return screen
36 | },
37 | action: {
38 | return Action.pathAction(id, action: $0)
39 | }
40 | )
41 | }
42 |
43 | public init(
44 | _ store: Store,
45 | root: @escaping () -> Root,
46 | destination: @escaping (Store) -> Destination
47 | ) {
48 | self.store = store
49 | self.root = root
50 | self.destination = destination
51 | }
52 |
53 | public var body: some View {
54 | WithViewStore(store, observe: { $0 }) { viewStore in
55 | RouteStack(
56 | viewStore.binding(
57 | get: \.paths,
58 | send: Action.updatePaths
59 | ),
60 | root: root,
61 | destination: { id, screen in
62 | destination(scopedStore(id: id, screen: screen))
63 | }
64 | )
65 | }
66 | }
67 | }
68 |
69 | extension Collection {
70 | subscript(safe index: Index) -> Element? {
71 | return indices.contains(index) ? self[index] : nil
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Example/Example/Router/RouterCore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouterCore.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 | //
8 |
9 | import ComposableArchitecture
10 | import TCARouteStack
11 | import Foundation
12 |
13 | public struct Router: Reducer {
14 | public struct State: Equatable, RouterState {
15 | public var root: Root.State
16 | public var paths: [RoutePath]
17 |
18 | public init(
19 | root: Root.State = .init(),
20 | paths: [RoutePath] = []
21 | ) {
22 | self.root = root
23 | self.paths = paths
24 | }
25 | }
26 |
27 | public enum Action: Equatable, RouterAction {
28 | case openURL(URL)
29 | case root(Root.Action)
30 | case updatePaths([RoutePath])
31 | case pathAction(RoutePath.ID, action: Screen.Action)
32 | }
33 |
34 | public var body: some Reducer {
35 | Reduce { state, action in
36 | switch action {
37 | case let .openURL(url):
38 | let value = url.valueOf("value") ?? ""
39 | var style: Style {
40 | switch value {
41 | case "push":
42 | return .push
43 | case "customSheet":
44 | return .sheet([.medium, .large], .visible)
45 | case "normalSheet":
46 | return .sheet()
47 | case "cover":
48 | return .cover
49 | default:
50 | return .cover
51 | }
52 | }
53 | switch url.host {
54 | case "back":
55 | state.paths.removeLast()
56 | case "backToRoot":
57 | state.paths.removeAll()
58 | case "firstView":
59 | state.paths.append(RoutePath(data: Screen.State.first(.init(value: value)), style: style))
60 | case "secondView":
61 | state.paths.append(RoutePath(data: Screen.State.second(.init(value: value)), style: style))
62 | case "thirdView":
63 | state.paths.append(RoutePath(data: Screen.State.third(.init(value: value)), style: style))
64 | default: break
65 | }
66 | return .none
67 | case .root:
68 | return .none
69 | case .updatePaths:
70 | return .none
71 | case .pathAction:
72 | return .none
73 | }
74 | }
75 | .forEachRoute {
76 | Screen()
77 | }
78 | Scope(state: \.root, action: /Action.root) { Root() }
79 | }
80 |
81 | public init() { }
82 | }
83 |
84 | public struct Screen: Reducer {
85 | public enum State: Equatable {
86 | case first(First.State)
87 | case second(Second.State)
88 | case third(Third.State)
89 | }
90 |
91 | public enum Action: Equatable {
92 | case first(First.Action)
93 | case second(Second.Action)
94 | case third(Third.Action)
95 | }
96 |
97 | public var body: some Reducer {
98 | Scope(state: /State.first, action: /Action.first) {
99 | First()
100 | }
101 | Scope(state: /State.second, action: /Action.second) {
102 | Second()
103 | }
104 | Scope(state: /State.third, action: /Action.third) {
105 | Third()
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "combine-schedulers",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/combine-schedulers",
7 | "state" : {
8 | "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a",
9 | "version" : "0.11.0"
10 | }
11 | },
12 | {
13 | "identity" : "routestack",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/Monsteel/RouteStack.git",
16 | "state" : {
17 | "revision" : "c3c89bea9ace2c9ec4a55baf20721ef3874e0321",
18 | "version" : "0.1.2"
19 | }
20 | },
21 | {
22 | "identity" : "swift-case-paths",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/pointfreeco/swift-case-paths",
25 | "state" : {
26 | "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984",
27 | "version" : "0.14.1"
28 | }
29 | },
30 | {
31 | "identity" : "swift-clocks",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/pointfreeco/swift-clocks",
34 | "state" : {
35 | "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d",
36 | "version" : "0.4.0"
37 | }
38 | },
39 | {
40 | "identity" : "swift-collections",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/apple/swift-collections",
43 | "state" : {
44 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
45 | "version" : "1.0.4"
46 | }
47 | },
48 | {
49 | "identity" : "swift-composable-architecture",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
52 | "state" : {
53 | "revision" : "4cf2104cf14d57ec3dfb8cbd90d29f2b81a32028",
54 | "version" : "0.58.0"
55 | }
56 | },
57 | {
58 | "identity" : "swift-concurrency-extras",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
61 | "state" : {
62 | "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795",
63 | "version" : "0.1.1"
64 | }
65 | },
66 | {
67 | "identity" : "swift-custom-dump",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/pointfreeco/swift-custom-dump",
70 | "state" : {
71 | "revision" : "4a87bb75be70c983a9548597e8783236feb3401e",
72 | "version" : "0.11.1"
73 | }
74 | },
75 | {
76 | "identity" : "swift-dependencies",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/pointfreeco/swift-dependencies",
79 | "state" : {
80 | "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee",
81 | "version" : "0.6.0"
82 | }
83 | },
84 | {
85 | "identity" : "swift-identified-collections",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/pointfreeco/swift-identified-collections",
88 | "state" : {
89 | "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29",
90 | "version" : "0.8.0"
91 | }
92 | },
93 | {
94 | "identity" : "swiftui-navigation",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/pointfreeco/swiftui-navigation",
97 | "state" : {
98 | "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12",
99 | "version" : "0.8.0"
100 | }
101 | },
102 | {
103 | "identity" : "xctest-dynamic-overlay",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
106 | "state" : {
107 | "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865",
108 | "version" : "0.9.0"
109 | }
110 | }
111 | ],
112 | "version" : 2
113 | }
114 |
--------------------------------------------------------------------------------
/Example/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Example
4 | //
5 | // Created by Tony on 2023/07/21.
6 | //
7 |
8 | import SwiftUI
9 | import RouteStack
10 |
11 | struct ContentView: View {
12 | @State var routePaths: [RoutePath] = []
13 |
14 | @ViewBuilder
15 | func root() -> some View {
16 |
17 | }
18 |
19 | var body: some View {
20 | RouteStack($routePaths, root: root) { id, path in
21 | switch path {
22 | case let .first(value):
23 | VStack(alignment: .leading) {
24 | Text("First View 입니다.")
25 | .font(.title)
26 |
27 | Text("\(value) 로 열렸습니다.")
28 | .font(.subheadline)
29 |
30 | Button("back") {
31 | // Deeplink를 통해 routePaths에 직접 접근하지 않고 이동할 수 있습니다.
32 | UIApplication.shared.open(URL(string: "routeStackExample://back")!)
33 | }
34 |
35 | Button("push") {
36 | routePaths.append(.init(data: Path.second("push"), style: .push))
37 | }
38 |
39 | Button("custom-sheet") {
40 | routePaths.append(.init(data: Path.second("custom-sheet"), style: .sheet([.medium, .large], .visible)))
41 | }
42 |
43 | Button("normal-sheet") {
44 | routePaths.append(.init(data: Path.second("normal-sheet"), style: .sheet()))
45 | }
46 |
47 | Button("cover") {
48 | routePaths.append(.init(data: Path.second("cover"), style: .cover))
49 | }
50 | }
51 | .frame(maxWidth: .infinity, maxHeight: .infinity)
52 | .background(Color.orange)
53 | case let .second(value):
54 | VStack(alignment: .leading) {
55 | Text("Second View 입니다.")
56 | .font(.title)
57 |
58 | Text("\(value) 로 열렸습니다.")
59 | .font(.subheadline)
60 |
61 | Button("back") {
62 | // Deeplink를 통해 routePaths에 직접 접근하지 않고 이동할 수 있습니다.
63 | UIApplication.shared.open(URL(string: "routeStackExample://back")!)
64 | }
65 |
66 | Button("push") {
67 | routePaths.append(.init(data: Path.third("push"), style: .push))
68 | }
69 |
70 | Button("custom-sheet") {
71 | routePaths.append(.init(data: Path.third("custom-sheet"), style: .sheet([.medium, .large], .visible)))
72 | }
73 |
74 | Button("normal-sheet") {
75 | routePaths.append(.init(data: Path.third("normal-sheet"), style: .sheet()))
76 | }
77 |
78 | Button("cover") {
79 | routePaths.append(.init(data: Path.third("cover"), style: .cover))
80 | }
81 | }
82 | .frame(maxWidth: .infinity, maxHeight: .infinity)
83 | .background(Color.yellow)
84 | case let .third(value):
85 | VStack(alignment: .leading) {
86 | Text("Third View 입니다.")
87 | .font(.title)
88 |
89 | Text("\(value) 로 열렸습니다.")
90 | .font(.subheadline)
91 |
92 | Button("back") {
93 | // Deeplink를 통해 routePaths에 직접 접근하지 않고 이동할 수 있습니다.
94 | UIApplication.shared.open(URL(string: "routeStackExample://back")!)
95 | }
96 |
97 | Button("back To root") {
98 | // Deeplink를 통해 routePaths에 직접 접근하지 않고 이동할 수 있습니다.
99 | UIApplication.shared.open(URL(string: "routeStackExample://backToRoot")!)
100 | }
101 | }
102 | .frame(maxWidth: .infinity, maxHeight: .infinity)
103 | .background(Color.green)
104 | }
105 | }
106 | .onOpenURL { url in
107 | switch url.host {
108 | case "back":
109 | routePaths.removeLast()
110 | case "backToRoot":
111 | routePaths.removeAll()
112 | default: break
113 | }
114 | }
115 | }
116 | }
117 |
118 | enum Path: Equatable {
119 | case first(String)
120 | case second(String)
121 | case third(String)
122 | }
123 |
124 | struct ContentView_Previews: PreviewProvider {
125 | static var previews: some View {
126 | ContentView()
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "combine-schedulers",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/combine-schedulers",
7 | "state" : {
8 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
9 | "version" : "1.0.0"
10 | }
11 | },
12 | {
13 | "identity" : "routestack",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/Monsteel/RouteStack.git",
16 | "state" : {
17 | "revision" : "c3c89bea9ace2c9ec4a55baf20721ef3874e0321",
18 | "version" : "0.1.2"
19 | }
20 | },
21 | {
22 | "identity" : "swift-case-paths",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/pointfreeco/swift-case-paths",
25 | "state" : {
26 | "revision" : "8d712376c99fc0267aa0e41fea732babe365270a",
27 | "version" : "1.3.3"
28 | }
29 | },
30 | {
31 | "identity" : "swift-clocks",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/pointfreeco/swift-clocks",
34 | "state" : {
35 | "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33",
36 | "version" : "1.0.2"
37 | }
38 | },
39 | {
40 | "identity" : "swift-collections",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/apple/swift-collections",
43 | "state" : {
44 | "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
45 | "version" : "1.1.0"
46 | }
47 | },
48 | {
49 | "identity" : "swift-composable-architecture",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
52 | "state" : {
53 | "revision" : "ae491c9e3f66631e72d58db8bb4c27dfc3d3afd4",
54 | "version" : "1.6.0"
55 | }
56 | },
57 | {
58 | "identity" : "swift-concurrency-extras",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras",
61 | "state" : {
62 | "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
63 | "version" : "1.1.0"
64 | }
65 | },
66 | {
67 | "identity" : "swift-custom-dump",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/pointfreeco/swift-custom-dump",
70 | "state" : {
71 | "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c",
72 | "version" : "1.3.0"
73 | }
74 | },
75 | {
76 | "identity" : "swift-dependencies",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/pointfreeco/swift-dependencies",
79 | "state" : {
80 | "revision" : "350e1e119babe8525f9bd155b76640a5de270184",
81 | "version" : "1.3.0"
82 | }
83 | },
84 | {
85 | "identity" : "swift-identified-collections",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/pointfreeco/swift-identified-collections",
88 | "state" : {
89 | "revision" : "2481e39ea43e14556ca9628259fa6b377427730c",
90 | "version" : "1.0.1"
91 | }
92 | },
93 | {
94 | "identity" : "swift-syntax",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/apple/swift-syntax",
97 | "state" : {
98 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
99 | "version" : "509.1.1"
100 | }
101 | },
102 | {
103 | "identity" : "swiftui-navigation",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/pointfreeco/swiftui-navigation",
106 | "state" : {
107 | "revision" : "2ec6c3a15293efff6083966b38439a4004f25565",
108 | "version" : "1.3.0"
109 | }
110 | },
111 | {
112 | "identity" : "xctest-dynamic-overlay",
113 | "kind" : "remoteSourceControl",
114 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
115 | "state" : {
116 | "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2",
117 | "version" : "1.1.2"
118 | }
119 | }
120 | ],
121 | "version" : 2
122 | }
123 |
--------------------------------------------------------------------------------
/Sources/TCARouteStack/Reducers/ForEachReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEachReducer.swift
3 | //
4 | //
5 | // Created by Tony on 2023/07/24.
6 | //
7 |
8 | import ComposableArchitecture
9 | import RouteStack
10 | import Foundation
11 |
12 | extension Reducer {
13 | public func forEachRoute(
14 | @ReducerBuilder element: () -> Element,
15 | file: StaticString = #file,
16 | fileID: StaticString = #fileID,
17 | line: UInt = #line
18 | ) -> some Reducer
19 | where
20 | ElementState == Element.State, ElementAction == Element.Action,
21 | Self.State: RouterState, Self.Action: RouterAction,
22 | Self.Action.ScreenAction == ElementAction,
23 | Self.Action.Screen == ElementState, Self.State.Screen == ElementState
24 | {
25 | CombineReducers {
26 | _ForEachRoutePathIDReducer(
27 | parent: self,
28 | toElementsState: \.paths,
29 | toElementAction: /Self.Action.pathAction,
30 | element: element(),
31 | file: file,
32 | fileID: fileID,
33 | line: line
34 | )
35 |
36 | _UpdatePathsOnInteraction(
37 | reducer: self,
38 | updatePaths: /Action.updatePaths,
39 | toLocalState: \.paths
40 | )
41 | }
42 | }
43 | }
44 |
45 | struct _UpdatePathsOnInteraction: Reducer {
46 | typealias State = Parent.State
47 | typealias Action = Parent.Action
48 |
49 | let reducer: Parent
50 | let updatePaths: AnyCasePath
51 | let toLocalState: WritableKeyPath
52 |
53 | func reduce(into state: inout Parent.State, action: Parent.Action) -> Effect {
54 | if let routes = updatePaths.extract(from: action) {
55 | state[keyPath: toLocalState] = routes
56 | }
57 | return .none
58 | }
59 | }
60 |
61 |
62 | struct _ForEachRoutePathIDReducer<
63 | Parent: Reducer,
64 | Element: Reducer
65 | >: Reducer
66 | where
67 | Parent.State: RouterState,
68 | Parent.Action: RouterAction,
69 | Element.State: Equatable,
70 | Element.Action: Equatable,
71 | Element.State == Parent.State.Screen
72 | {
73 | typealias ID = RoutePath.ID
74 |
75 | let parent: Parent
76 | let toElementsState: WritableKeyPath]>
77 | let toElementAction: AnyCasePath
78 | let element: Element
79 | let file: StaticString
80 | let fileID: StaticString
81 | let line: UInt
82 |
83 | init(
84 | parent: Parent,
85 | toElementsState: WritableKeyPath]>,
86 | toElementAction: AnyCasePath,
87 | element: Element,
88 | file: StaticString,
89 | fileID: StaticString,
90 | line: UInt
91 | ) {
92 | self.parent = parent
93 | self.toElementsState = toElementsState
94 | self.toElementAction = toElementAction
95 | self.element = element
96 | self.file = file
97 | self.fileID = fileID
98 | self.line = line
99 | }
100 |
101 | public func reduce(
102 | into state: inout Parent.State, action: Parent.Action
103 | ) -> Effect {
104 | self.reduceForEach(into: &state, action: action)
105 | .merge(with: self.parent.reduce(into: &state, action: action))
106 | }
107 |
108 | func reduceForEach(
109 | into state: inout Parent.State, action: Parent.Action
110 | ) -> Effect {
111 | guard let (id, elementAction) = self.toElementAction.extract(from: action) else { return .none }
112 | let array = state[keyPath: self.toElementsState]
113 | guard let index = array.firstIndex(where: { $0.id == id }) else { return .none }
114 |
115 | if array[safe: index] == nil {
116 | runtimeWarn(
117 | """
118 | A "forEachRoute" at "\(self.fileID):\(self.line)" received an action for a screen at \
119 | index \(index) but the screens array only contains \(array.count) elements.
120 |
121 | Action:
122 | \(action)
123 |
124 | This may be because a parent reducer (e.g. coordinator reducer) removed the screen at \
125 | this index before the action was sent.
126 | """,
127 | file: self.file,
128 | line: self.line
129 | )
130 | return .none
131 | }
132 | return self.element
133 | .reduce(into: &state[keyPath: self.toElementsState][index].data, action: elementAction)
134 | .map { self.toElementAction.embed((id, $0)) }
135 | }
136 | }
137 |
138 | func runtimeWarn(
139 | _ message: @autoclosure () -> String,
140 | file: StaticString? = nil,
141 | line: UInt? = nil
142 | ) {
143 | #if DEBUG
144 | let message = message()
145 | if _XCTIsTesting {
146 | if let file = file, let line = line {
147 | XCTFail(message, file: file, line: line)
148 | } else {
149 | XCTFail(message)
150 | }
151 | } else {
152 | let formatter: DateFormatter = {
153 | let formatter = DateFormatter()
154 | formatter.dateFormat = "yyyy-MM-dd HH:MM:SS.sssZ"
155 | return formatter
156 | }()
157 | fputs("\(formatter.string(from: Date())) [TCARouteStack] \(message)\n", stderr)
158 | }
159 | #endif
160 | }
161 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # TCARouteStack
2 |
3 | #### TCARouteStack allows you to manage navigation and presentation states as a single stack in SwiftUI.
4 |
5 | TCARouteStack allows you to manage navigation and presentation states as a single stack in SwiftUI.
6 | 💁🏻♂️ It supports iOS 16 and later.
7 | 💁🏻♂️ Implemented purely using SwiftUI.
8 | 💁🏻♂️ Based on NavigationStack implementation.
9 | 💁🏻♂️ Supports various options for sheets (Presentation Detents, Presentation Drag Indicator).
10 |
11 | ## Advantages
12 |
13 | ✅ With TCARouteStack, you can easily apply the Coordinator pattern in SwiftUI + TCA.
14 | ✅ By using RouteStack and Deeplinks together, you can achieve a one-to-one relationship between scenes and deeplinks.
15 | ✅ TCARouteStack allows you to easily present views using different methods such as push, sheet, cover, etc.
16 |
17 | ## Base
18 |
19 | This project is built on top of [RouteStack](https://github.com/Monsteel/RouteStack).
20 | For more information, please refer to the documentation of the respective library.
21 |
22 | ## How to Use
23 |
24 | You can achieve sophisticated routing with simple code.
25 | For detailed usage, please refer to the [example code](https://github.com/Monsteel/TCARouteStack/tree/main/Example).
26 |
27 | ### Basic Structure
28 |
29 | #### RouterView
30 |
31 | ```swift
32 | public struct RouterView: View {
33 | @ObservedObject
34 | private var viewStore: ViewStoreOf
35 | private let store: StoreOf
36 |
37 | public init(store: StoreOf) {
38 | self.viewStore = ViewStoreOf(store)
39 | self.store = store
40 | }
41 |
42 | private func root() -> some View {
43 | // Omitted
44 | }
45 |
46 | public var body: some View {
47 | RouteStackStore(store, root: root) { store in
48 | SwitchStore(store) { state in
49 | switch state {
50 | case .first:
51 | CaseLet(/Screen.State.first, action: Screen.Action.first, then: FirstView.init)
52 |
53 | case .second:
54 | CaseLet(/Screen.State.second, action: Screen.Action.second, then: SecondView.init)
55 |
56 | case .third:
57 | CaseLet(/Screen.State.third, action: Screen.Action.third, then: ThirdView.init)
58 | }
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | #### RouterCore
66 |
67 | ```swift
68 | public struct Router: Reducer {
69 | public struct State: Equatable, RouterState {
70 | public var paths: [RoutePath]
71 | }
72 |
73 | public enum Action: Equatable, RouterAction {
74 | case updatePaths([RoutePath])
75 | case pathAction(RoutePath.ID, action: Screen.Action)
76 | }
77 |
78 | public var body: some Reducer {
79 | Reduce { state, action in
80 | switch action {
81 | case .updatePaths:
82 | return .none
83 | case .pathAction:
84 | return .none
85 | }
86 | }
87 | .forEachRoute {
88 | Screen()
89 | }
90 | Scope(state: \.root, action: /Action.root) { Root() }
91 | }
92 | }
93 |
94 | public struct Screen: Reducer {
95 | public enum State: Equatable {
96 | case first(First.State)
97 | case second(Second.State)
98 | case third(Third.State)
99 | }
100 |
101 | public enum Action: Equatable {
102 | case first(First.Action)
103 | case second(Second.Action)
104 | case third(Third.Action)
105 | }
106 |
107 | public var body: some Reducer {
108 | Scope(state: /State.first, action: /Action.first) {
109 | First()
110 | }
111 | Scope(state: /State.second, action: /Action.second) {
112 | Second()
113 | }
114 | Scope(state: /State.third, action: /Action.third) {
115 | Third()
116 | }
117 | }
118 | }
119 |
120 | ```
121 |
122 | ### Screen Transition with Deeplinks
123 |
124 | You can use deeplinks to transition between screens without dependencies between them.
125 |
126 | #### RouterView
127 |
128 | ```swift
129 | public struct RouterView: View {
130 | public var body: some View {
131 | RouteStackStore(store, root: root) { store in
132 | // Omitted
133 | }.onOpenURL { url in
134 | viewStore.send(.openURL(url))
135 | }
136 | }
137 | }
138 | ```
139 |
140 | #### RouterCore
141 |
142 | ```swift
143 | public struct Router: Reducer {
144 | public struct State: Equatable, RouterState {
145 | public var paths: [RoutePath]
146 | // 생략
147 | }
148 |
149 | public enum Action: Equatable, RouterAction {
150 | case openURL(URL)
151 | // 생략
152 | }
153 |
154 | public var body: some Reducer {
155 | Reduce { state, action in
156 | switch action {
157 | case let .openURL(url):
158 | switch url.host {
159 | case "back":
160 | state.paths.removeLast()
161 | case "backToRoot":
162 | state.paths.removeAll()
163 | case "firstView":
164 | state.paths.append(RoutePath(data: Screen.State.first(.init()), style: .cover))
165 | case "secondView":
166 | state.paths.append(RoutePath(data: Screen.State.second(.init()), style: .push))
167 | case "thirdView":
168 | state.paths.append(RoutePath(data: Screen.State.third(.init()), style: .sheet([.medium, .large], .visible)))
169 | default: break
170 | }
171 | return .none
172 | }
173 |
174 | // Omitted
175 | }
176 | .forEachRoute {
177 | Screen()
178 | }
179 | }
180 | }
181 |
182 | ```
183 |
184 | ## Let's Build Together
185 |
186 | I'm open to contributions and improvements for anything that can be enhanced.
187 | Feel free to contribute through Pull Requests. 🙏
188 |
189 | ## License
190 |
191 | TCARouteStack is available under the MIT license. See the [LICENSE](https://github.com/Monsteel/TCARouteStack/tree/main/LICENSE) file for more info.
192 |
193 | ## Auther
194 |
195 | Tony | dev.e0eun@gmail.com
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TCARouteStack
2 |
3 | #### SwiftUI + TCA 에서 navigation 과 presentation 상태를 하나의 Stack으로 관리할 수 있습니다.
4 |
5 | [There is also an explanation in English.](https://github.com/Monsteel/TCARouteStack/tree/main/README_EN.md)
6 |
7 | 💁🏻♂️ iOS16+ 를 지원합니다.
8 | 💁🏻♂️ 순수한 SwiftUI 를 사용하여 구현되었습니다.
9 | 💁🏻♂️ NavigationStack을 기반으로 하여 구현되었습니다.
10 | 💁🏻♂️ sheet의 다양한 옵션(Presentation Detents, Presentation Drag Indicator)을 지원합니다.
11 |
12 | ## 장점
13 |
14 | ✅ TCARouteStack을 사용하면, SwiftUI + TCA 환경에서 **Coordinator 개념을 쉽게 적용시킬 수 있습니다.**
15 | ✅ TCARouteStack과 Deeplink를 함께 사용하여, **화면(Scene) : Deeplink = 1:1 을 구현할 수 있습니다.**
16 | ✅ TCARouteStack을 사용하면, 상황에 따라 노출 방식(push, sheet, cover..)을 선택하여 **쉽게 보여줄 수 있습니다.**
17 |
18 | ## 기반
19 |
20 | 이 프로젝트는 [RouteStack](https://github.com/Monsteel/RouteStack)을 기반으로 구현되었습니다.
21 | 보다 자세한 내용은 해당 라이브러리의 문서를 참고해 주세요
22 |
23 | ## 사용방법
24 |
25 | 간단한 코드로, 멋진 Routing을 구현할 수 있습니다.
26 | 자세한 사용방법은 [예제코드](https://github.com/Monsteel/TCARouteStack/tree/main/Example)를 참고해주세요.
27 |
28 | ### 기본 구조
29 |
30 | #### RouterView
31 |
32 | ```swift
33 | public struct RouterView: View {
34 | @ObservedObject
35 | private var viewStore: ViewStoreOf
36 | private let store: StoreOf
37 |
38 | public init(store: StoreOf) {
39 | self.viewStore = ViewStoreOf(store)
40 | self.store = store
41 | }
42 |
43 | private func root() -> some View {
44 | // 생략
45 | }
46 |
47 | public var body: some View {
48 | RouteStackStore(store, root: root) { store in
49 | SwitchStore(store) { state in
50 | switch state {
51 | case .first:
52 | CaseLet(/Screen.State.first, action: Screen.Action.first, then: FirstView.init)
53 |
54 | case .second:
55 | CaseLet(/Screen.State.second, action: Screen.Action.second, then: SecondView.init)
56 |
57 | case .third:
58 | CaseLet(/Screen.State.third, action: Screen.Action.third, then: ThirdView.init)
59 | }
60 | }
61 | }
62 | }
63 | }
64 | ```
65 |
66 | #### RouterCore
67 |
68 | ```swift
69 | public struct Router: Reducer {
70 | public struct State: Equatable, RouterState {
71 | public var paths: [RoutePath]
72 | }
73 |
74 | public enum Action: Equatable, RouterAction {
75 | case updatePaths([RoutePath])
76 | case pathAction(RoutePath.ID, action: Screen.Action)
77 | }
78 |
79 | public var body: some Reducer {
80 | Reduce { state, action in
81 | switch action {
82 | case .updatePaths:
83 | return .none
84 | case .pathAction:
85 | return .none
86 | }
87 | }
88 | .forEachRoute {
89 | Screen()
90 | }
91 | Scope(state: \.root, action: /Action.root) { Root() }
92 | }
93 | }
94 |
95 | public struct Screen: Reducer {
96 | public enum State: Equatable {
97 | case first(First.State)
98 | case second(Second.State)
99 | case third(Third.State)
100 | }
101 |
102 | public enum Action: Equatable {
103 | case first(First.Action)
104 | case second(Second.Action)
105 | case third(Third.Action)
106 | }
107 |
108 | public var body: some Reducer {
109 | Scope(state: /State.first, action: /Action.first) {
110 | First()
111 | }
112 | Scope(state: /State.second, action: /Action.second) {
113 | Second()
114 | }
115 | Scope(state: /State.third, action: /Action.third) {
116 | Third()
117 | }
118 | }
119 | }
120 |
121 | ```
122 |
123 | ### deeplink를 사용한 화면 전환 방법
124 |
125 | deeplink를 활용해 화면 간 의존성 없이 화면을 전환할 수 있습니다.
126 |
127 | #### RouterView
128 |
129 | ```swift
130 | public struct RouterView: View {
131 | public var body: some View {
132 | RouteStackStore(store, root: root) { store in
133 | // 생략
134 | }.onOpenURL { url in
135 | viewStore.send(.openURL(url))
136 | }
137 | }
138 | }
139 | ```
140 |
141 | #### RouterCore
142 |
143 | ```swift
144 | public struct Router: Reducer {
145 | public struct State: Equatable, RouterState {
146 | public var paths: [RoutePath]
147 | // 생략
148 | }
149 |
150 | public enum Action: Equatable, RouterAction {
151 | case openURL(URL)
152 | // 생략
153 | }
154 |
155 | public var body: some Reducer {
156 | Reduce { state, action in
157 | switch action {
158 | case let .openURL(url):
159 | switch url.host {
160 | case "back":
161 | state.paths.removeLast()
162 | case "backToRoot":
163 | state.paths.removeAll()
164 | case "firstView":
165 | state.paths.append(RoutePath(data: Screen.State.first(.init()), style: .cover))
166 | case "secondView":
167 | state.paths.append(RoutePath(data: Screen.State.second(.init()), style: .push))
168 | case "thirdView":
169 | state.paths.append(RoutePath(data: Screen.State.third(.init()), style: .sheet([.medium, .large], .visible)))
170 | default: break
171 | }
172 | return .none
173 | }
174 |
175 | // 생략
176 | }
177 | .forEachRoute {
178 | Screen()
179 | }
180 | }
181 | }
182 |
183 | ```
184 |
185 | ## Swift Package Manager(SPM) 을 통해 사용할 수 있어요
186 |
187 | ```swift
188 | dependencies: [
189 | .package(url: "https://github.com/Monsteel/TCARouteStack.git", .upToNextMajor(from: "0.0.1"))
190 | ]
191 | ```
192 |
193 | ## 함께 만들어 나가요
194 |
195 | 개선의 여지가 있는 모든 것들에 대해 열려있습니다.
196 | PullRequest를 통해 기여해주세요. 🙏
197 |
198 | ## License
199 |
200 | TCARouteStack 는 MIT 라이선스로 이용할 수 있습니다. 자세한 내용은 [라이선스](https://github.com/Monsteel/TCARouteStack/tree/main/LICENSE) 파일을 참조해 주세요.
201 | TCARouteStack is available under the MIT license. See the [LICENSE](https://github.com/Monsteel/TCARouteStack/tree/main/LICENSE) file for more info.
202 |
203 | ## Auther
204 |
205 | 이영은(Tony) | dev.e0eun@gmail.com
206 |
207 | [](https://hits.seeyoufarm.com)
208 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | EA48E6392A6E7B2E00DF0E1B /* TCARouteStack in Frameworks */ = {isa = PBXBuildFile; productRef = EA48E6382A6E7B2E00DF0E1B /* TCARouteStack */; };
11 | EA48E63C2A6E7B2F00DF0E1B /* Accessibility.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA48E63A2A6E7B2E00DF0E1B /* Accessibility.framework */; };
12 | EA48E63D2A6E7B2F00DF0E1B /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA48E63B2A6E7B2F00DF0E1B /* Accelerate.framework */; };
13 | EA7910422A6E6BEC00F35E6E /* RootCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910402A6E6BEC00F35E6E /* RootCore.swift */; };
14 | EA7910432A6E6BEC00F35E6E /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910412A6E6BEC00F35E6E /* RootView.swift */; };
15 | EA7910482A6E6C0A00F35E6E /* FirstCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910462A6E6C0A00F35E6E /* FirstCore.swift */; };
16 | EA7910492A6E6C0A00F35E6E /* FirstView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910472A6E6C0A00F35E6E /* FirstView.swift */; };
17 | EA79104C2A6E6C1100F35E6E /* SecondCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA79104A2A6E6C1100F35E6E /* SecondCore.swift */; };
18 | EA79104D2A6E6C1100F35E6E /* SecondView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA79104B2A6E6C1100F35E6E /* SecondView.swift */; };
19 | EA7910522A6E6C2300F35E6E /* RouterCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910502A6E6C2300F35E6E /* RouterCore.swift */; };
20 | EA7910532A6E6C2300F35E6E /* RouterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910512A6E6C2300F35E6E /* RouterView.swift */; };
21 | EA7910572A6E714C00F35E6E /* ThirdCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910552A6E714C00F35E6E /* ThirdCore.swift */; };
22 | EA7910582A6E714C00F35E6E /* ThirdView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910562A6E714C00F35E6E /* ThirdView.swift */; };
23 | EA79105A2A6E726300F35E6E /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7910592A6E726300F35E6E /* Dependencies.swift */; };
24 | EA79105C2A6E77C000F35E6E /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA79105B2A6E77C000F35E6E /* URL+Extensions.swift */; };
25 | EA90A9342A6A163A00C92BAC /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA90A9332A6A163A00C92BAC /* ExampleApp.swift */; };
26 | EA90A9362A6A163A00C92BAC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA90A9352A6A163A00C92BAC /* ContentView.swift */; };
27 | EA90A9382A6A163B00C92BAC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA90A9372A6A163B00C92BAC /* Assets.xcassets */; };
28 | EA90A93C2A6A163C00C92BAC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA90A93B2A6A163C00C92BAC /* Preview Assets.xcassets */; };
29 | /* End PBXBuildFile section */
30 |
31 | /* Begin PBXFileReference section */
32 | EA48E63A2A6E7B2E00DF0E1B /* Accessibility.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accessibility.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.4.sdk/System/Library/Frameworks/Accessibility.framework; sourceTree = DEVELOPER_DIR; };
33 | EA48E63B2A6E7B2F00DF0E1B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.4.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; };
34 | EA71E4232A6A185C0028ACE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
35 | EA7910402A6E6BEC00F35E6E /* RootCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCore.swift; sourceTree = ""; };
36 | EA7910412A6E6BEC00F35E6E /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; };
37 | EA7910462A6E6C0A00F35E6E /* FirstCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstCore.swift; sourceTree = ""; };
38 | EA7910472A6E6C0A00F35E6E /* FirstView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstView.swift; sourceTree = ""; };
39 | EA79104A2A6E6C1100F35E6E /* SecondCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondCore.swift; sourceTree = ""; };
40 | EA79104B2A6E6C1100F35E6E /* SecondView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondView.swift; sourceTree = ""; };
41 | EA7910502A6E6C2300F35E6E /* RouterCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterCore.swift; sourceTree = ""; };
42 | EA7910512A6E6C2300F35E6E /* RouterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterView.swift; sourceTree = ""; };
43 | EA7910552A6E714C00F35E6E /* ThirdCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdCore.swift; sourceTree = ""; };
44 | EA7910562A6E714C00F35E6E /* ThirdView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdView.swift; sourceTree = ""; };
45 | EA7910592A6E726300F35E6E /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; };
46 | EA79105B2A6E77C000F35E6E /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; };
47 | EA79105D2A6E7AD800F35E6E /* TCARouteStack */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TCARouteStack; path = ..; sourceTree = ""; };
48 | EA90A9302A6A163A00C92BAC /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
49 | EA90A9332A6A163A00C92BAC /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
50 | EA90A9352A6A163A00C92BAC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
51 | EA90A9372A6A163B00C92BAC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
52 | EA90A9392A6A163B00C92BAC /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; };
53 | EA90A93B2A6A163C00C92BAC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
54 | /* End PBXFileReference section */
55 |
56 | /* Begin PBXFrameworksBuildPhase section */
57 | EA90A92D2A6A163A00C92BAC /* Frameworks */ = {
58 | isa = PBXFrameworksBuildPhase;
59 | buildActionMask = 2147483647;
60 | files = (
61 | EA48E63D2A6E7B2F00DF0E1B /* Accelerate.framework in Frameworks */,
62 | EA48E6392A6E7B2E00DF0E1B /* TCARouteStack in Frameworks */,
63 | EA48E63C2A6E7B2F00DF0E1B /* Accessibility.framework in Frameworks */,
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXFrameworksBuildPhase section */
68 |
69 | /* Begin PBXGroup section */
70 | EA71E4212A6A18500028ACE9 /* Packages */ = {
71 | isa = PBXGroup;
72 | children = (
73 | EA79105D2A6E7AD800F35E6E /* TCARouteStack */,
74 | );
75 | name = Packages;
76 | sourceTree = "";
77 | };
78 | EA71E4242A6A18620028ACE9 /* Frameworks */ = {
79 | isa = PBXGroup;
80 | children = (
81 | EA48E63B2A6E7B2F00DF0E1B /* Accelerate.framework */,
82 | EA48E63A2A6E7B2E00DF0E1B /* Accessibility.framework */,
83 | );
84 | name = Frameworks;
85 | sourceTree = "";
86 | };
87 | EA79103E2A6E6B4800F35E6E /* Configs */ = {
88 | isa = PBXGroup;
89 | children = (
90 | EA71E4232A6A185C0028ACE9 /* Info.plist */,
91 | EA90A9372A6A163B00C92BAC /* Assets.xcassets */,
92 | EA90A9392A6A163B00C92BAC /* Example.entitlements */,
93 | );
94 | path = Configs;
95 | sourceTree = "";
96 | };
97 | EA79103F2A6E6BD100F35E6E /* Scene */ = {
98 | isa = PBXGroup;
99 | children = (
100 | EA7910542A6E714700F35E6E /* Third */,
101 | EA79104E2A6E6C1400F35E6E /* Second */,
102 | EA7910452A6E6BF800F35E6E /* First */,
103 | EA7910442A6E6BF000F35E6E /* Root */,
104 | );
105 | path = Scene;
106 | sourceTree = "";
107 | };
108 | EA7910442A6E6BF000F35E6E /* Root */ = {
109 | isa = PBXGroup;
110 | children = (
111 | EA7910402A6E6BEC00F35E6E /* RootCore.swift */,
112 | EA7910412A6E6BEC00F35E6E /* RootView.swift */,
113 | );
114 | path = Root;
115 | sourceTree = "";
116 | };
117 | EA7910452A6E6BF800F35E6E /* First */ = {
118 | isa = PBXGroup;
119 | children = (
120 | EA7910462A6E6C0A00F35E6E /* FirstCore.swift */,
121 | EA7910472A6E6C0A00F35E6E /* FirstView.swift */,
122 | );
123 | path = First;
124 | sourceTree = "";
125 | };
126 | EA79104E2A6E6C1400F35E6E /* Second */ = {
127 | isa = PBXGroup;
128 | children = (
129 | EA79104A2A6E6C1100F35E6E /* SecondCore.swift */,
130 | EA79104B2A6E6C1100F35E6E /* SecondView.swift */,
131 | );
132 | path = Second;
133 | sourceTree = "";
134 | };
135 | EA79104F2A6E6C1E00F35E6E /* Router */ = {
136 | isa = PBXGroup;
137 | children = (
138 | EA7910502A6E6C2300F35E6E /* RouterCore.swift */,
139 | EA7910512A6E6C2300F35E6E /* RouterView.swift */,
140 | );
141 | path = Router;
142 | sourceTree = "";
143 | };
144 | EA7910542A6E714700F35E6E /* Third */ = {
145 | isa = PBXGroup;
146 | children = (
147 | EA7910552A6E714C00F35E6E /* ThirdCore.swift */,
148 | EA7910562A6E714C00F35E6E /* ThirdView.swift */,
149 | );
150 | path = Third;
151 | sourceTree = "";
152 | };
153 | EA90A9272A6A163A00C92BAC = {
154 | isa = PBXGroup;
155 | children = (
156 | EA71E4212A6A18500028ACE9 /* Packages */,
157 | EA90A9322A6A163A00C92BAC /* Example */,
158 | EA90A9312A6A163A00C92BAC /* Products */,
159 | EA71E4242A6A18620028ACE9 /* Frameworks */,
160 | );
161 | sourceTree = "";
162 | };
163 | EA90A9312A6A163A00C92BAC /* Products */ = {
164 | isa = PBXGroup;
165 | children = (
166 | EA90A9302A6A163A00C92BAC /* Example.app */,
167 | );
168 | name = Products;
169 | sourceTree = "";
170 | };
171 | EA90A9322A6A163A00C92BAC /* Example */ = {
172 | isa = PBXGroup;
173 | children = (
174 | EA79103E2A6E6B4800F35E6E /* Configs */,
175 | EA90A9332A6A163A00C92BAC /* ExampleApp.swift */,
176 | EA7910592A6E726300F35E6E /* Dependencies.swift */,
177 | EA90A9352A6A163A00C92BAC /* ContentView.swift */,
178 | EA79105B2A6E77C000F35E6E /* URL+Extensions.swift */,
179 | EA79104F2A6E6C1E00F35E6E /* Router */,
180 | EA79103F2A6E6BD100F35E6E /* Scene */,
181 | EA90A93A2A6A163C00C92BAC /* Preview Content */,
182 | );
183 | path = Example;
184 | sourceTree = "";
185 | };
186 | EA90A93A2A6A163C00C92BAC /* Preview Content */ = {
187 | isa = PBXGroup;
188 | children = (
189 | EA90A93B2A6A163C00C92BAC /* Preview Assets.xcassets */,
190 | );
191 | path = "Preview Content";
192 | sourceTree = "";
193 | };
194 | /* End PBXGroup section */
195 |
196 | /* Begin PBXNativeTarget section */
197 | EA90A92F2A6A163A00C92BAC /* Example */ = {
198 | isa = PBXNativeTarget;
199 | buildConfigurationList = EA90A93F2A6A163C00C92BAC /* Build configuration list for PBXNativeTarget "Example" */;
200 | buildPhases = (
201 | EA90A92C2A6A163A00C92BAC /* Sources */,
202 | EA90A92D2A6A163A00C92BAC /* Frameworks */,
203 | EA90A92E2A6A163A00C92BAC /* Resources */,
204 | );
205 | buildRules = (
206 | );
207 | dependencies = (
208 | );
209 | name = Example;
210 | packageProductDependencies = (
211 | EA48E6382A6E7B2E00DF0E1B /* TCARouteStack */,
212 | );
213 | productName = Example;
214 | productReference = EA90A9302A6A163A00C92BAC /* Example.app */;
215 | productType = "com.apple.product-type.application";
216 | };
217 | /* End PBXNativeTarget section */
218 |
219 | /* Begin PBXProject section */
220 | EA90A9282A6A163A00C92BAC /* Project object */ = {
221 | isa = PBXProject;
222 | attributes = {
223 | BuildIndependentTargetsInParallel = 1;
224 | LastSwiftUpdateCheck = 1430;
225 | LastUpgradeCheck = 1430;
226 | TargetAttributes = {
227 | EA90A92F2A6A163A00C92BAC = {
228 | CreatedOnToolsVersion = 14.3;
229 | };
230 | };
231 | };
232 | buildConfigurationList = EA90A92B2A6A163A00C92BAC /* Build configuration list for PBXProject "Example" */;
233 | compatibilityVersion = "Xcode 14.0";
234 | developmentRegion = en;
235 | hasScannedForEncodings = 0;
236 | knownRegions = (
237 | en,
238 | Base,
239 | );
240 | mainGroup = EA90A9272A6A163A00C92BAC;
241 | productRefGroup = EA90A9312A6A163A00C92BAC /* Products */;
242 | projectDirPath = "";
243 | projectRoot = "";
244 | targets = (
245 | EA90A92F2A6A163A00C92BAC /* Example */,
246 | );
247 | };
248 | /* End PBXProject section */
249 |
250 | /* Begin PBXResourcesBuildPhase section */
251 | EA90A92E2A6A163A00C92BAC /* Resources */ = {
252 | isa = PBXResourcesBuildPhase;
253 | buildActionMask = 2147483647;
254 | files = (
255 | EA90A93C2A6A163C00C92BAC /* Preview Assets.xcassets in Resources */,
256 | EA90A9382A6A163B00C92BAC /* Assets.xcassets in Resources */,
257 | );
258 | runOnlyForDeploymentPostprocessing = 0;
259 | };
260 | /* End PBXResourcesBuildPhase section */
261 |
262 | /* Begin PBXSourcesBuildPhase section */
263 | EA90A92C2A6A163A00C92BAC /* Sources */ = {
264 | isa = PBXSourcesBuildPhase;
265 | buildActionMask = 2147483647;
266 | files = (
267 | EA7910432A6E6BEC00F35E6E /* RootView.swift in Sources */,
268 | EA79105C2A6E77C000F35E6E /* URL+Extensions.swift in Sources */,
269 | EA79104C2A6E6C1100F35E6E /* SecondCore.swift in Sources */,
270 | EA79104D2A6E6C1100F35E6E /* SecondView.swift in Sources */,
271 | EA79105A2A6E726300F35E6E /* Dependencies.swift in Sources */,
272 | EA7910492A6E6C0A00F35E6E /* FirstView.swift in Sources */,
273 | EA7910582A6E714C00F35E6E /* ThirdView.swift in Sources */,
274 | EA7910422A6E6BEC00F35E6E /* RootCore.swift in Sources */,
275 | EA7910482A6E6C0A00F35E6E /* FirstCore.swift in Sources */,
276 | EA90A9362A6A163A00C92BAC /* ContentView.swift in Sources */,
277 | EA7910522A6E6C2300F35E6E /* RouterCore.swift in Sources */,
278 | EA7910572A6E714C00F35E6E /* ThirdCore.swift in Sources */,
279 | EA90A9342A6A163A00C92BAC /* ExampleApp.swift in Sources */,
280 | EA7910532A6E6C2300F35E6E /* RouterView.swift in Sources */,
281 | );
282 | runOnlyForDeploymentPostprocessing = 0;
283 | };
284 | /* End PBXSourcesBuildPhase section */
285 |
286 | /* Begin XCBuildConfiguration section */
287 | EA90A93D2A6A163C00C92BAC /* Debug */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | ALWAYS_SEARCH_USER_PATHS = NO;
291 | CLANG_ANALYZER_NONNULL = YES;
292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
294 | CLANG_ENABLE_MODULES = YES;
295 | CLANG_ENABLE_OBJC_ARC = YES;
296 | CLANG_ENABLE_OBJC_WEAK = YES;
297 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
298 | CLANG_WARN_BOOL_CONVERSION = YES;
299 | CLANG_WARN_COMMA = YES;
300 | CLANG_WARN_CONSTANT_CONVERSION = YES;
301 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
302 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
303 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
304 | CLANG_WARN_EMPTY_BODY = YES;
305 | CLANG_WARN_ENUM_CONVERSION = YES;
306 | CLANG_WARN_INFINITE_RECURSION = YES;
307 | CLANG_WARN_INT_CONVERSION = YES;
308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
309 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
310 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
311 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
312 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
314 | CLANG_WARN_STRICT_PROTOTYPES = YES;
315 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
317 | CLANG_WARN_UNREACHABLE_CODE = YES;
318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
319 | COPY_PHASE_STRIP = NO;
320 | DEBUG_INFORMATION_FORMAT = dwarf;
321 | ENABLE_STRICT_OBJC_MSGSEND = YES;
322 | ENABLE_TESTABILITY = YES;
323 | GCC_C_LANGUAGE_STANDARD = gnu11;
324 | GCC_DYNAMIC_NO_PIC = NO;
325 | GCC_NO_COMMON_BLOCKS = YES;
326 | GCC_OPTIMIZATION_LEVEL = 0;
327 | GCC_PREPROCESSOR_DEFINITIONS = (
328 | "DEBUG=1",
329 | "$(inherited)",
330 | );
331 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
332 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
333 | GCC_WARN_UNDECLARED_SELECTOR = YES;
334 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
335 | GCC_WARN_UNUSED_FUNCTION = YES;
336 | GCC_WARN_UNUSED_VARIABLE = YES;
337 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
338 | MTL_FAST_MATH = YES;
339 | ONLY_ACTIVE_ARCH = YES;
340 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
341 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
342 | };
343 | name = Debug;
344 | };
345 | EA90A93E2A6A163C00C92BAC /* Release */ = {
346 | isa = XCBuildConfiguration;
347 | buildSettings = {
348 | ALWAYS_SEARCH_USER_PATHS = NO;
349 | CLANG_ANALYZER_NONNULL = YES;
350 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
352 | CLANG_ENABLE_MODULES = YES;
353 | CLANG_ENABLE_OBJC_ARC = YES;
354 | CLANG_ENABLE_OBJC_WEAK = YES;
355 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
356 | CLANG_WARN_BOOL_CONVERSION = YES;
357 | CLANG_WARN_COMMA = YES;
358 | CLANG_WARN_CONSTANT_CONVERSION = YES;
359 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
360 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
361 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
362 | CLANG_WARN_EMPTY_BODY = YES;
363 | CLANG_WARN_ENUM_CONVERSION = YES;
364 | CLANG_WARN_INFINITE_RECURSION = YES;
365 | CLANG_WARN_INT_CONVERSION = YES;
366 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
367 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
368 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
370 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
371 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
372 | CLANG_WARN_STRICT_PROTOTYPES = YES;
373 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
374 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
375 | CLANG_WARN_UNREACHABLE_CODE = YES;
376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
377 | COPY_PHASE_STRIP = NO;
378 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
379 | ENABLE_NS_ASSERTIONS = NO;
380 | ENABLE_STRICT_OBJC_MSGSEND = YES;
381 | GCC_C_LANGUAGE_STANDARD = gnu11;
382 | GCC_NO_COMMON_BLOCKS = YES;
383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
385 | GCC_WARN_UNDECLARED_SELECTOR = YES;
386 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
387 | GCC_WARN_UNUSED_FUNCTION = YES;
388 | GCC_WARN_UNUSED_VARIABLE = YES;
389 | MTL_ENABLE_DEBUG_INFO = NO;
390 | MTL_FAST_MATH = YES;
391 | SWIFT_COMPILATION_MODE = wholemodule;
392 | SWIFT_OPTIMIZATION_LEVEL = "-O";
393 | };
394 | name = Release;
395 | };
396 | EA90A9402A6A163C00C92BAC /* Debug */ = {
397 | isa = XCBuildConfiguration;
398 | buildSettings = {
399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
400 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
401 | CODE_SIGN_ENTITLEMENTS = Example/Configs/Example.entitlements;
402 | CODE_SIGN_STYLE = Automatic;
403 | CURRENT_PROJECT_VERSION = 1;
404 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
405 | DEVELOPMENT_TEAM = 62D52RWVMH;
406 | ENABLE_HARDENED_RUNTIME = YES;
407 | ENABLE_PREVIEWS = YES;
408 | GENERATE_INFOPLIST_FILE = YES;
409 | INFOPLIST_FILE = Example/Configs/Info.plist;
410 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
411 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
412 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
413 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
414 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
415 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
416 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
417 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
418 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
419 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
420 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
421 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
422 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
423 | MACOSX_DEPLOYMENT_TARGET = 13.3;
424 | MARKETING_VERSION = 1.0;
425 | PRODUCT_BUNDLE_IDENTIFIER = tcaRouteStackExample.com;
426 | PRODUCT_NAME = "$(TARGET_NAME)";
427 | SDKROOT = auto;
428 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
429 | SUPPORTS_MACCATALYST = NO;
430 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
431 | SWIFT_EMIT_LOC_STRINGS = YES;
432 | SWIFT_VERSION = 5.0;
433 | TARGETED_DEVICE_FAMILY = 1;
434 | };
435 | name = Debug;
436 | };
437 | EA90A9412A6A163C00C92BAC /* Release */ = {
438 | isa = XCBuildConfiguration;
439 | buildSettings = {
440 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
441 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
442 | CODE_SIGN_ENTITLEMENTS = Example/Configs/Example.entitlements;
443 | CODE_SIGN_STYLE = Automatic;
444 | CURRENT_PROJECT_VERSION = 1;
445 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
446 | DEVELOPMENT_TEAM = 62D52RWVMH;
447 | ENABLE_HARDENED_RUNTIME = YES;
448 | ENABLE_PREVIEWS = YES;
449 | GENERATE_INFOPLIST_FILE = YES;
450 | INFOPLIST_FILE = Example/Configs/Info.plist;
451 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
452 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
453 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
454 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
455 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
456 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
457 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
458 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
461 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
462 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
463 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
464 | MACOSX_DEPLOYMENT_TARGET = 13.3;
465 | MARKETING_VERSION = 1.0;
466 | PRODUCT_BUNDLE_IDENTIFIER = tcaRouteStackExample.com;
467 | PRODUCT_NAME = "$(TARGET_NAME)";
468 | SDKROOT = auto;
469 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
470 | SUPPORTS_MACCATALYST = NO;
471 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
472 | SWIFT_EMIT_LOC_STRINGS = YES;
473 | SWIFT_VERSION = 5.0;
474 | TARGETED_DEVICE_FAMILY = 1;
475 | };
476 | name = Release;
477 | };
478 | /* End XCBuildConfiguration section */
479 |
480 | /* Begin XCConfigurationList section */
481 | EA90A92B2A6A163A00C92BAC /* Build configuration list for PBXProject "Example" */ = {
482 | isa = XCConfigurationList;
483 | buildConfigurations = (
484 | EA90A93D2A6A163C00C92BAC /* Debug */,
485 | EA90A93E2A6A163C00C92BAC /* Release */,
486 | );
487 | defaultConfigurationIsVisible = 0;
488 | defaultConfigurationName = Release;
489 | };
490 | EA90A93F2A6A163C00C92BAC /* Build configuration list for PBXNativeTarget "Example" */ = {
491 | isa = XCConfigurationList;
492 | buildConfigurations = (
493 | EA90A9402A6A163C00C92BAC /* Debug */,
494 | EA90A9412A6A163C00C92BAC /* Release */,
495 | );
496 | defaultConfigurationIsVisible = 0;
497 | defaultConfigurationName = Release;
498 | };
499 | /* End XCConfigurationList section */
500 |
501 | /* Begin XCSwiftPackageProductDependency section */
502 | EA48E6382A6E7B2E00DF0E1B /* TCARouteStack */ = {
503 | isa = XCSwiftPackageProductDependency;
504 | productName = TCARouteStack;
505 | };
506 | /* End XCSwiftPackageProductDependency section */
507 | };
508 | rootObject = EA90A9282A6A163A00C92BAC /* Project object */;
509 | }
510 |
--------------------------------------------------------------------------------