Composable navigation beta 2 #2048
Replies: 19 comments 70 replies
-
OH SNAP! |
Beta Was this translation helpful? Give feedback.
-
Great to see how TCA is evolving 🤘🏻 |
Beta Was this translation helpful? Give feedback.
-
FYI, the new case study does not appear in the Navigation section. I ended up replacing the root with that specific case study and then I changed it to "Go to A → B → C → D → E" (ie, 2 extra levels) to check whether the SwiftUI bug of no more than 3 levels still applies, and it doesn't! It looks like the path approach (as opposed to the nested destination approach) does not have that bug, meaning that deep-linking into a screen/scene deeper than 3 levels now works. 🍾 🎆 Thank you both for all the great work you've been doing. |
Beta Was this translation helpful? Give feedback.
-
With this latest update, I'm wondering if the proper way to do "tree-based" navigation (as opposed to the stack-based option) is still to use a
It seems to be happening because the parent store is receiving a |
Beta Was this translation helpful? Give feedback.
-
Hello again :) Following up on this thread, I wrote the same sort of toy app to test various navigation flows using
and, once again, I noticed two issues. Before describing them, it will be important to note that the following set of actions works as expected:
Now for the issues. Issue 1
Issue 2
A questionLastly, I have a question. How can I write a test to verify that the scenes presented during a deep-link fire their actions (if any) correctly? In this toy app, as each scene appears, they fire an view
.task { ViewStore(store.stateless).send(.onTask) } In a test, however, no scene view is actually loaded so that Of course, in the test, I could explicitly fire that action myself but that seems to me to assume the very action I want to test. So, more generally, is there a way to test that a view will indeed fire that As always, thank you greatly for any help. |
Beta Was this translation helpful? Give feedback.
-
I have another question. If the root scene is "navigation central" and scenes are essentially independent of each other, how does a scene scope its children's states and pass them up to the root so the root can push the children scenes with their appropriate states? One mechanism I can imagine is this: Say that scene A wants to present scene B and in scene A's view there's a button to present scene B. Scene A will have a This looks very complicated to me. For each child scene a parent can present, there will have to be 2 actions, one without and one with the child state as the enum case associated type. And then the parent's reducer will have to handle all of those pairs in essentially the same way: get the first, use it to hydrate the child state, fire the second, handle the second by ignoring it and letting it percolate up to the root. Is there a better way? Update: Another mechanism I just thought about is for each parent scene to have public getters for their children's states, which the root scene can then use to get the appropriate state. This cuts the actions from 2 to 1 per child scene. Can anyone offer better alternatives? |
Beta Was this translation helpful? Give feedback.
-
These stack state changes are very exciting. Unfortunately in our case we have complicated navigation between different screens and cannot use After a few days of trying various things, I'm stumped on how we can get similar behavior to the The goal is to have various screens (
I have seen some older discussions from a few years ago about this topic but was hoping there might be some more thinking/suggestions around this problem now that these tools are being previewed and the framework has evolved |
Beta Was this translation helpful? Give feedback.
-
Hello again :) I'm having an issue where alert and confirmation dialog actions aren't being fired. Here's part of the code (sample app attached): public struct UserSettings: Reducer {
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .logoutAlertButtonTapped:
var alertState = AlertState<Action>(
title: TextState("Are you sure?"),
message: TextState("Please confirm that you would like to log out.")
)
alertState.buttons = [
.destructive(TextState("Log out"), action: .send(.delegate(.performLogout))),
.cancel(TextState("Cancel"))
]
state.destination = .logoutAlert(alertState)
return .none
case .logoutConfirmationDialigButtonTapped:
var confirmationDialogState = ConfirmationDialogState<Action>(
title: TextState("Are you sure?"),
message: TextState("Please confirm that you would like to log out.")
)
confirmationDialogState.buttons = [
.destructive(TextState("Log out"), action: .send(.delegate(.performLogout))),
.cancel(TextState("Cancel"))
]
state.destination = .logoutConfirmationDialog(confirmationDialogState)
return .none
case .destination:
return .none
// // Even this isn't executed.
// case .delegate(.performLogout):
// return .none
case .delegate:
return .none
}
}
.ifLet(\.$destination, action: /Action.destination) {
Destination()
}
}
} The parent scene, public struct AppFeature: Reducer {
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .presentUserSettingsButtonTapped:
let userSettingsState = UserSettings.State()
state.destination = .userSettings(userSettingsState)
return .none
case .destination(.presented(.userSettings(.delegate(.performLogout)))):
state.logoutStatus = "Requested"
state.destination = nil
return .none
case .destination:
return .none
}
}
.ifLet(\.$destination, action: /Action.destination) {
Destination()
}
}
} I've been staring at my code but can't see anything obviously wrong. Any ideas? Many thanks in advance. |
Beta Was this translation helpful? Give feedback.
-
I was wondering if it is possible to adopt the new navigation machinery of There's a couple reasons I am interested in this. First, I know Second, less academically, I think it couples me to what To work around this, I've fallen back to the vanilla SwiftUI import ComposableArchitecture
import SwiftUI
struct VanillaAlertTest: Reducer {
struct State: Equatable {
@BindingState var alertInput: String = ""
@PresentationState var destination: Destination.State?
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case destination(PresentationAction<Destination.Action>)
case presentAlertButtonTapped
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .destination(.presented(.alert(.confirm))),
.destination(.dismiss):
state.alertInput = ""
return .none
case .destination:
return .none
case .presentAlertButtonTapped:
state.destination = .alert
return .none
}
}
.ifLet(\.$destination, action: /Action.destination) {
Destination()
}
}
struct Destination: Reducer {
enum State: Equatable {
case alert
}
enum Action {
case alert(Alert)
enum Alert: Equatable {
case confirm
}
}
var body: some ReducerOf<Self> {
Scope(state: /State.alert, action: /Action.alert) {}
}
}
}
struct VanillaAlertView: View {
let store: StoreOf<VanillaAlertTest>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Button("Present Alert") { viewStore.send(.presentAlertButtonTapped) }
.alert(
"Alert",
isPresented: Binding(
get: { viewStore.destination == .alert },
set: { isPresented in
if !isPresented, viewStore.state.destination != nil {
viewStore.send(.destination(.dismiss))
}
}
)
) {
TextField("Text Input", text: viewStore.binding(\.$alertInput))
Button("Cancel", role: .cancel) { viewStore.send(.destination(.dismiss)) }
Button("Confirm") { viewStore.send(.destination(.presented(.alert(.confirm)))) }
}
}
}
}
struct VanillaAlertTest_Previews: PreviewProvider {
static var previews: some View {
VanillaAlertView(
store: .init(
initialState: .init(),
reducer: VanillaAlertTest()._printChanges()
)
)
}
} Have I missed tools shipping with the navigation beta that would support this use? Is there interest? If not, I understand; it's a little bikeshed-y. Is what I've sketched out above the recommended approach? |
Beta Was this translation helpful? Give feedback.
-
First of all. Thank you @stephencelis & @mbrandonw for your amazing work, and that the library also supports
Click here for the example codeHome.swiftpublic struct Home: ReducerProtocol {
public struct State: Equatable {
@PresentationState var categories: Categories.State?
@PresentationState var settings: Settings.State?
}
public enum Action: Equatable {
case categories(PresentationAction<Categories.Action>)
case categoriesButtonTapped
case settings(PresentationAction<Settings.Action>)
case settingsButtonTapped
}
public init() {}
public var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .categories:
return .none
case .categoriesButtonTapped:
state.categories = Categories.State()
return .none
case .settings:
return .none
case .settingsButtonTapped:
state.settings = Settings.State()
return .none
}
}
.ifLet(\.$categories, action: /Action.categories) {
Categories()
}
.ifLet(\.$settings, action: /Action.settings) {
Settings()
}
}
}
public struct HomeView: View {
let store: StoreOf<Home>
public var body: some View {
WithViewStore(self.store) { viewStore in
NavigationStack {
VStack {
Button("Smart Practice") {
viewStore.send(.categoriesButtonTapped)
}
}
.toolbar {
Button {
viewStore.send(.settingsButtonTapped)
} label: {
Image(systemName: "gear")
}
}
.sheet(
store: self.store.scope(state: \.$categories, action: Home.Action.categories)
) { store in
CategoriesView(store: store)
}
.sheet(
store: self.store.scope(state: \.$settings, action: Home.Action.settings)
) { store in
SettingsView(store: store)
}
}
}
}
} Categories.swiftpublic struct Categories: ReducerProtocol {
public struct State: Equatable {
public var categoryContents: IdentifiedArrayOf<Category.State>
public var categoryContentsLoading: Bool
public init(
categoryContents: IdentifiedArrayOf<Category.State> = [],
categoryContentsLoading: Bool = false
) {
self.categoryContents = categoryContents
self.categoryContentsLoading = categoryContentsLoading
}
}
public enum Action: Equatable {
case category(id: Category.State.ID, action: Category.Action)
case loadCategories
case loadCategoriesCompleted([CategoryData])
}
@Dependency(\.client) var client
@Dependency(\.dismiss) var dismiss
public var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .category:
return .none
case .loadCategories:
state.categoryContentsLoading = true
// state.categoryContents = <generated-placeholder-content>
return .task {
.loadCategoriesCompleted(try await self.client.getCategories())
}
case let .loadCategoriesCompleted(categoryContents):
state.categoryContentsLoading = false
state.categoryContents = IdentifiedArray(
uniqueElements: categoryContents.map { categoryData in
Category.State(categoryData: categoryData)
})
return .none
}
}
.forEach(\.categoryContents, action: /Action.category) {
Category()
}
}
}
public struct CategoriesView: View {
let store: StoreOf<Categories>
public var body: some View {
WithViewStore(self.store) { viewStore in
NavigationStack {
VStack {
ScrollView {
ForEachStore(
self.store.scope(
state: \.categoryContents, action: Categories.Action.category(id:action:))
) { categoryStore in
CategoryView(store: categoryStore)
}
}
}
.redacted(if: viewStore.categoryContentsLoading == true)
}
}
.onAppear {
viewStore.send(.loadCategories)
}
}
} |
Beta Was this translation helpful? Give feedback.
-
It looks like this line is causing the library not to build in tvOS
|
Beta Was this translation helpful? Give feedback.
-
Thanks for such a great TCA updates. Could you please help me with resolving the issue that I get when trying to archive app, when debug builds work fine. See an error on screenshot. |
Beta Was this translation helpful? Give feedback.
-
I'm pretty confident there is an issue when deeplinking to a sheet containing a stack (using Visible on the simulator on iOS 16.4 on my side. I'll try on other sims/devices. on the prerelease/1.0 freshly pulled. @stephencelis @mbrandonw I'm not sure I you preferred that I post here or open an issue as this is related to the beta. |
Beta Was this translation helpful? Give feedback.
-
First of all, thank you @stephencelis & @mbrandonw for putting so much work into this amazing library. I have a conceptual question about the new NavigationStackStore-APIs. Feature B works on and potentially modifies some substate of A and Feature C works on and potentially modifies some substate of B. As in the case-study, the "Path" is only available at the root-feature in A. The only solutions I can think of seem rather complicated (e.g. sharing the Path-State and observing changes or observing path-actions). Scoping (and pullbacking if that's a word) the State of B to get to C would be much more direct, but I don't see how it is possible using a NavigationStackStore. Am I missing something? This seems to be a common use case. |
Beta Was this translation helpful? Give feedback.
-
Hey, I'm really excited about the navigation support in Composable Architecture!! As I've been playing around with the example, I noticed that if an alert is displayed when operating the NavigationStack in the standups project, it seems to reappear as it cannot process the .destination(.dismiss) action. I think this project is a common use case in iOS app development, and it seems valuable to identify the cause of this problem.
|
Beta Was this translation helpful? Give feedback.
-
Has any one made attempts to mix SwiftUI navigation apis backports and TCA ? I just tried using https://github.com/lm/navigation-stack-backport to poke the idea here (don't mind the code it's an experiment on my lunch break) but it seems to mostly work. I was able to Deeplink multiples levels on iOS 14 and 15. Previews are crashing for some reason on iOS 14 but work ok on iOS 15+. My goal was to check the bug discussed above and there is a warning and the animation is kinda broken but it's working. There is another lib https://github.com/johnpatrickmorgan/NavigationBackport which is based on SwiftUI, I'll try to do the same with it. My "implementation" obviously uses TCA private apis so I can't easily make it into a lib. Could something like be considered for merging into TCA maybe using SPI ? |
Beta Was this translation helpful? Give feedback.
-
Hey! We are trying to present a fullScreenCover immediately after presenting a sheet in SwiftUI. Sadly it seems that there is a SwiftUI bug that leads to the problem that the fullScreenCover is rather shown as a sheet. Now after refactoring that code to leverage the new navigation APIs there is no possibility to hook into the |
Beta Was this translation helpful? Give feedback.
-
@mbrandonw Great job guys, thank you a lot 🎉 . Just a quick question, I am considering using a composable navigation beta version on a real ongoing project, when are you guys planning to release this version? |
Beta Was this translation helpful? Give feedback.
-
Hi, Scheme: Problem:
|
Beta Was this translation helpful? Give feedback.
-
It's been just under two months since we kicked off our navigation beta, in which we released an assortment of tools to manage presentation in the Composable Architecture. This included tools for dealing with alerts, confirmation dialogs, sheets, popovers, fullscreen covers, pre-iOS 16 navigation links, and the tree-based
navigationDestination
view modifier. The beta notably did not provide tools for iOS 16'sNavigationStack
, but that changes today.Composable Stack Navigation Basics
Like last time, we're not going to give a detailed overview of these new tools and how we motivated or designed them (see the forthcoming episodes for that 😉), and documentation is still in-progress, but here is a very quick overview of the stack-based tools and how to use them.
When a root feature contains a navigation stack of elements to be presented, you will enhance its domain using the new
StackState
andStackAction
types:StackState
is a collection type that is specialized for navigation operations in the Composable Architecture, where each element represents a screen in the stack that is powered by its own reducer. It is similar to SwiftUI'sNavigationPath
, and has many of the same operations likeappend
andremoveLast
, but it is not type-erased: you can freely inspect and mutate the data inside.Then you will make use of the new, special
forEach
reducer operator that can single out the stack state and action, and run the child feature on that element when it is active:That's all that is needed as far as the domain and reducer is concerned. The
forEach
operator has been with the library since the beginning, but is now enhanced with super powers, including automatically cancelling child effects when they are dismissed, and more.The last step is in the view, where the library provides a new
NavigationStackStore
view, which powers aNavigationStack
and its destinations using a store.That's the basics. There's a whole lot more to learn, but we will leave it at that for now, and we encourage you to explore the updated branch when you get a chance.
Trying the beta
These new stack-based tools are already available on the
navigation-beta
branch. If you've been testing things so far, you can pull the latest and immediately make use of these tools. The 1.0 preview likewise has been updated with the latest, greatest tools.We hope these tools fill a gap in the library and make it ready for its first major release.
As always, if you take things for a spin, please let us know (via Twitter, Mastodon, GitHub discussions, or our new Slack community) if you have questions, comments, concerns, or suggestions!
Beta Was this translation helpful? Give feedback.
All reactions