Replies: 7 comments 31 replies
-
Thanks @mbrandonw! import CasePaths
import ComposableArchitecture
import SwiftUI
// NOTE: Utility for quicker state scopes
@available(iOS 16.0, *)
extension Array where Element == Main.Route {
func find<Value>(_ casePath: CasePath<Element, Value>) -> Value? {
compactMap { element in
guard let value = casePath.extract(from: element) else { return nil }
return value
}.first
}
mutating func update<Value>(_ casePath: CasePath<Element, Value>, with value: Value)
where Value: Equatable {
guard
let routeIndex = firstIndex(where: { element in
guard casePath.extract(from: element) != .none else { return false }
return true
})
else {
return
}
self[routeIndex] = casePath.embed(value)
}
}
@available(iOS 16.0, *)
struct Main {
enum Route: Hashable {
case second(SecondPage.State)
case third(ThirdPage.State)
}
struct State: Equatable {
var navigation: [Route]
var secondPage: SecondPage.State? {
get { navigation.find(/Route.second) }
set { newValue.map { navigation.update(/Route.second, with: $0) } }
}
var thirdPage: ThirdPage.State? {
get { navigation.find(/Route.third) }
set { newValue.map { navigation.update(/Route.third, with: $0) } }
}
}
enum Action {
case actionTapped
case navigationPathChanged([Route])
case secondPage(SecondPage.Action)
case thirdPage(ThirdPage.Action)
}
static let localReducer: Reducer<State, Action, Void> = .init { state, action, _ in
switch action {
case .actionTapped:
let route = Main.Route.second(.init(content: "Content"))
state.navigation.append(route)
case .navigationPathChanged(let navigation):
state.navigation = navigation
case .secondPage(let secondPageAction):
switch secondPageAction {
case .didNavigateOnThirdPage:
let route = Main.Route.third(.init(content: "Content3"))
state.navigation.append(route)
}
case .thirdPage:
()
}
return .none
}
static let reducer: Reducer<State, Action, Void> = .combine(
SecondPage.reducer.optional().pullback(
state: \.secondPage,
action: /Action.secondPage,
environment: { _ in }
),
ThirdPage.reducer.optional().pullback(
state: \.thirdPage,
action: /Action.thirdPage,
environment: { _ in }
),
localReducer
)
struct View: SwiftUI.View {
let store: Store<State, Action>
var body: some SwiftUI.View {
WithViewStore(store) { viewStore in
NavigationStack(
path: viewStore.binding(get: \.navigation, send: Action.navigationPathChanged)
) {
Button(
"Click me to navigate", action: { viewStore.send(.actionTapped) }
)
.navigationDestination(for: Route.self, destination: { route in
switch route {
case .second:
IfLetStore(
store.scope(
state: \.secondPage,
action: Action.secondPage
)
) { store in
SecondPage.View(store: store)
}
case .third:
IfLetStore(
store.scope(
state: \.thirdPage,
action: Action.thirdPage
)
) { store in
ThirdPage.View(store: store)
}
}
})
.navigationTitle("Navigation")
}
}
}
}
}
@available(iOS 16.0, *)
struct SecondPage {
struct State: Equatable, Hashable {
var content: String
}
enum Action {
case didNavigateOnThirdPage
}
static let reducer: Reducer<State, Action, Void> = .empty
struct View: SwiftUI.View {
let store: Store<State, Action>
var body: some SwiftUI.View {
WithViewStore(store) { viewStore in
Button("Second Page - Go tho third page") {
viewStore.send(.didNavigateOnThirdPage)
}
}
}
}
}
@available(iOS 16.0, *)
struct ThirdPage {
struct State: Equatable, Hashable {
var content: String
}
enum Action {
case buttonTapped
}
static let reducer: Reducer<State, Action, Void> = .init { state, action, _ in
switch action {
case .buttonTapped:
state.content = "Tapped!"
}
return .none
}
struct View: SwiftUI.View {
let store: Store<State, Action>
var body: some SwiftUI.View {
WithViewStore(store) { viewStore in
Button("Third view \(viewStore.content)") {
viewStore.send(.buttonTapped)
}
}
}
}
} Navigation is fully driven by reducer and the subviews are totally Navigation-free. There's still some caveats. |
Beta Was this translation helpful? Give feedback.
-
Regarding the issue with programmatic navigation initiated by If none of these feedbacks is addressed positively, and as a proof of concept, I've made the following |
Beta Was this translation helpful? Give feedback.
-
Someone captured all questions and answers from Apple in WWDC Lounges here. There is a category for Navigation, maybe some answers from Apple can provide useful insight regarding the new navigation API. |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, we have some early explorations to share. It's pretty rough, but also promising. We think some of the tools will be made even nicer once we finish up the protocol reducer experimentation we have been working on. You can run the case studies app to see some simple usage. The API is probably going to change quite a bit over the next few weeks/months, but it follows the style we've pushed for bindable state/actions as well as our nav experiments that some have found in our branches. Essentially there are reducer and view helpers that help you combine each feature's domain into the root domain. We've also been filing Feedbacks of things we think would help make better TCA nav tools, and you can find those here. |
Beta Was this translation helpful? Give feedback.
-
Hey! All is not lost, however. It turns out that if we're able to read the last component of an active path, we have enough API surface to turn it into a mutable random access collection, or more simply said, an For this to work, the Because we don't want to conform path.inspectable // A `NavigationPath.Inspectable` collection
// or
path.inspectable(of: Int.self) // A `NavigationPath.Inspectable.Of<Int>` collection Both are mutable, range-replaceable, random access collections. It means that most of Array's API's are automatically available, even some crazy ones like Both are exposing a @State var path = NavigationPath().inspectable
var body: some View {
NavigationStack(path: $path.navigationPath) {
…
}
} From there, it works exactly like a Some rough edges Components need to be
As we can see, all the issues are coming the codable trick. If we only had access to the last path component of a Concerning TCA navigation, how much this can be useful depends on the route that will be taken, but our control over You can find a gist with all of this here. The whole content can be copy/pasted to replace the content of a ContentView.swift in a new SwiftUI app project. |
Beta Was this translation helpful? Give feedback.
-
Edit: this turned out to be a bit of a dead end for decoupling or improving navigation generally. But it is still nice as a way to remove boilerplate from features that use I've been looking into decoupling strategies for our project as our team is increasing in size, and of course this ends up being very closely linked to navigation with SwiftUI. Definitely excited for what's being cooked up here! But we (and probably a lot of others) have a hard requirement to support n-2 iOS version. That means we're still a couple of years away from being able to use this for decoupling features :( For us, I've been playing around with higher order TCA feature wrappers of the SwiftUI components like For example: struct NavigationLinkState<
LabelState: Equatable & Identifiable,
DestinationState: Equatable & Identifiable
>: Equatable, Identifiable {
var id: UUID = .init()
var label: LabelState
var destination: DestinationState?
var isDestinationPresented: Bool = false
}
enum NavigationLinkAction<
LabelAction: Equatable,
DestinationAction: Equatable
>: Equatable {
case setNewIsDestinationPresented(Bool)
case label(LabelAction)
case destination(DestinationAction)
}
struct NavigationLinkEnvironment<
LabelEnvironment,
DestinationEnvironment
> {
let makeLabelEnvironment: () -> LabelEnvironment
let makeDestinationEnvironment: () -> DestinationEnvironment
var label: LabelEnvironment {
makeLabelEnvironment()
}
var destination: DestinationEnvironment {
makeDestinationEnvironment()
}
}
func navigationLinkReducer<
LabelState: Equatable & Identifiable,
LabelAction: Equatable,
LabelEnvironment,
DestinationState: Equatable & Identifiable,
DestinationAction: Equatable,
DestinationEnvironment
>(
labelReducer: Reducer<LabelState, LabelAction, LabelEnvironment>,
destinationReducer: Reducer<DestinationState, DestinationAction, DestinationEnvironment>,
makeDestinationState: @escaping (NavigationLinkState<LabelState, DestinationState>) -> DestinationState
) -> Reducer<
NavigationLinkState<LabelState, DestinationState>,
NavigationLinkAction<LabelAction, DestinationAction>,
NavigationLinkEnvironment<LabelEnvironment, DestinationEnvironment>
> {
.combine(
.init { state, action, _ in
switch action {
case .setNewIsDestinationPresented(let isPresented):
state.destination = isPresented ? makeDestinationState(state) : nil
state.isDestinationPresented = isPresented
return .none
case .label, .destination:
return .none
}
},
labelReducer.pullback(
state: \.label,
action: /NavigationLinkAction.label,
environment: \.label
),
destinationReducer.optional().pullback(
state: \.destination,
action: /NavigationLinkAction.destination,
environment: \.destination
)
)
}
struct NavigationLinkView<
LabelState: Equatable & Identifiable,
LabelAction: Equatable,
LabelView: View,
DestinationState: Equatable & Identifiable,
DestinationAction: Equatable,
DestinationView: View
>: View {
typealias NavigationLinkStore = Store<
NavigationLinkState<LabelState, DestinationState>,
NavigationLinkAction<LabelAction, DestinationAction>
>
let store: NavigationLinkStore
typealias LabelStore = Store<LabelState, LabelAction>
let makeLabelView: (LabelStore) -> LabelView
typealias DestinationStore = Store<DestinationState, DestinationAction>
let makeDestinationView: (DestinationStore) -> DestinationView
init(
store: NavigationLinkStore,
@ViewBuilder labelView makeLabelView: @escaping (LabelStore) -> LabelView,
@ViewBuilder destinationView makeDestinationView: @escaping (DestinationStore) -> DestinationView
) {
self.store = store
self.makeLabelView = makeLabelView
self.makeDestinationView = makeDestinationView
}
var body: some View {
WithViewStore(store) { viewStore in
NavigationLink(
isActive: viewStore.binding(
get: \.isDestinationPresented,
send: NavigationLinkAction.setNewIsDestinationPresented
),
destination: {
IfLetStore(store.scope(state: \.destination, action: NavigationLinkAction.destination)) {
makeDestinationView($0)
}
},
label: {
makeLabelView(store.scope(state: \.label, action: NavigationLinkAction.label))
}
)
}
}
} And then you use it like: struct AppState: Equatable {
var person: NavigationLinkState<PersonRowState, PersonDetailsState>
}
enum AppAction: Equatable {
case person(NavigationLinkAction<PersonRowAction, PersonDetailsAction>)
}
struct AppEnvironment {
var person: NavigationLinkEnvironment<PersonRowEnvironment, PersonDetailsEnvironment> {
.init(
makeLabelEnvironment: { .init() },
makeDestinationEnvironment: { .init() }
)
}
}
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
.empty,
navigationLinkReducer(
labelReducer: personRowReducer,
destinationReducer: personDetailsReducer,
makeDestinationState: { appState in
// Opportunity to put some logic here to build the right destination state from parent state
.init()
}
).pullback(
state: \.person,
action: /AppAction.person,
environment: \.person
)
)
struct AppView: View {
let store: Store<AppState, AppAction>
var body: some View {
NavigationView {
NavigationLinkView(
store: store.scope(state: \.person, action: AppAction.person),
labelView: { PersonRowView(store: $0) },
destinationView: { PersonDetailsView(store: $0) }
)
}
}
} So now we can have a fully built, tested and merged Question for you all: what am I missing? This seems nice to me, but I've only been working with TCA for 8 months. I might be missing gotchas that will rear their ugly heads when it's too late... 😱 |
Beta Was this translation helpful? Give feedback.
-
Hello everyone. Over the past few weeks I have tried several approaches but none have worked. Whenever you share states between screens, it stops working: iOS 16 Navigation API feedbacks Apple is shipping the last betas before the official release. I have two apps that use TCA but I can't launch them in the App Store because they are broken on iOS16. My main concern is that the architecture will remain broken until I find a solution to the new opaque navigation API in iOS16. Any hope? I will keep trying :( |
Beta Was this translation helpful? Give feedback.
-
As everyone knows, SwiftUI 4 comes with some new navigation APIs that seem to solve some bugs (such as drilling down multiple levels), but it's also structured in a way that doesn't play nicely with modeling navigation as a tree of state. We're still exploring ideas, but there was some really good discussion in a closed PR on swiftui-navigation that I want to highlight.
If anyone else has done explorations please feel free to add any of your findings in this discussion!
Beta Was this translation helpful? Give feedback.
All reactions