具有 TCA 的堆栈和模态有状态导航框架

路由减速机

路由Reducer是一个框架,用于使用TCA对堆栈和模态进行有状态导航。它松散地受到协调器模式的启发,可以逐渐采用并递归使用。

分期付款

路由Reducer可通过Swift Package Manager获得。

dependencies: [
  .package(url: "https://github.com/mtzaquia/RoutingReducer.git", .upToNextMajor(from: "0.0.11")),
],

用法

路由Reducer在可组合架构(TCA)之上构建了开发人员友好的导航模式。它试图为那些已经使用TCA的人使用熟悉的概念,并被设计为附加到现有的化简器实现,假设他们已经在使用最新的方法。ReducerProtocol

路由化简器协议

是给定流的基础。它将用于构建根视图并处理其定义的路由中可用的所有可能的导航路由。RoutingReducerProtocol

它本质上是一个 ,但有额外的要求:ReducerProtocol

  • 必须符合StateRoutingState
  • mut 符合ActionRoutingAction
  • 必须声明调用的类型,并且它必须符合RouteRouting
  • 代替 ,您需要提供 和函数来桥接化简器操作到具体的导航操作。bodyrootBodyrouteBody

struct MyRouter: RoutingReducerProtocol {
    enum Route: Routing {
        // the available routes for this reducer, their actions and IDs. 
    }

    struct State: RoutingState {
        let id = UUID() // declaring an arbitrary ID is fine.
        var navigation: Route.NavigationState = .init()
        var root: MyRoot.State
    }
    
    enum Action: RoutingAction {
        case navigation(Route.NavigationAction)
        case route(UUID, Route.Action)
        case modalRoute(Route.Action)
        case root(MyRoot.Action)
    }

    var rootBody: some RootReducer<Self> {
        MyRoot()
    }
    
    var routeBody: some RouteReducer<Self> {
        Scope(
            state: /Route.first,
            action: /Route.Action.first,
            First.init
        )
        // other scoped reducers for all routes declared on this reducer... 
    }
    
    func navigation(for action: Action) -> Route.NavigationAction? {
        // a closure resolving this reducer's actions to a navigation action.
    }
}

Routing

The protocol defines all available routes for a given flow, and is a required conformance to the type on conformances.RoutingRouteRoutingReducerProtocol

It is important to mention that routes require an ID. The way the ID is defined will determine whether the same view can be pushed/presented more than once on the same flow. That’s why states belonging to a flow must conform to .RoutedState

For example, the conformance below allows for the same screen to be pushed multiple times, as it uses the state’s ID to identify itself (assuming namely IDs are different):

enum Route: Routing {
    case first(First.State)
    case second(Second.State)

    enum Action {
        case first(First.Action)
        case second(Second.Action)
    }

    var id: UUID {
        switch self {
            case .first(let state): return state.id
            case .second(let state): return state.id
        }
    }
}

Whereas the example below will never allow for to be presented more than once:first

enum Route: Routing {
    case first(First.State)

    enum Action {
        case first(First.Action)
    }

    var id: UUID {
        switch self {
            case .first: return "first"
        }
    }
}

WithRoutingStore

Every view that is the base of a given router should hold a store of that reducer and extract the current presentation state using the special view, as well as provide all possible views via the parameter.WithRoutingStoreroutes:

struct MyRouterView: View {
    let store: StoreOf<MyModalRouter>
    var body: some View {
        WithRoutingStore(store) { rootStore, navigation, modal in
            // `rootStore` must be given to the root view of your router
            // `navigation` may be provided to a `RoutedNavigationStack` if you expect a navigation stack in your flow.
            // `modal` may be unwrapped and provided to SwiftUI modifiers for presenting a sheet, for instance.
            MyRootView(store: rootStore)
                .sheet(item: modal.item, content: modal.content)
        } routes: { store in
            SwitchStore(store) {
                CaseLet(
                    state: /MyModalRouter.Route.modal,
                    action: MyModalRouter.Route.Action.modal,
                    then: ModalView.init
                )
            }
        }
    }
}

RoutedNavigationStack

If you would like to deal with a stacked navigation, you should use as the top-level view in your declaration. This view will receive the navigation state extracted from your store using and the root view for the navigation flow.RoutedNavigationStackWithRoutingStoreWithRoutingStore

struct MyNavigationRouterView: View {
    let store: StoreOf<MyNavigationRouter>
    var body: some View {
        WithRoutingStore(store) { rootStore, navigation, _ in
            // Pass the `navigation` along to your `RoutedNavigationStack` initialiser.
            RoutedNavigationStack(navigation: navigation) {
                // The root view is now declared as part of the `RoutedNavigationStack`
                // and will act as the navigation root, receiving the `rootStore` instance.
                MyRootView(store: rootStore)
            }
        } routes: { store in
            // All your navigation destination views with SwitchStore/CaseLet...
        }
    }
}

You can, naturally, combine navigation and modality at will.

Known limitations and issues

  • There is no convenient way to use different modal presentations for different routes, though this can be achieved with a custom binding;
  • Effect cancellation needs to be handled manually;
  • Dismissing a screen while editing a value with will warn of unhandled actions.@BindingState

License

Copyright (c) 2023 @mtzaquia

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

GitHub

点击跳转