├── .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 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FMonsteel%2FTCARouteStack&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](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 | --------------------------------------------------------------------------------