I am trying to implement a SwiftUI application with NavigationView
and an option to dismiss to the root view using Swift Composable Architecure.
FirstView → present SecondView → present ThirdView → dismiss to FirstView
State of the FirstView
contains an optional state of the SecondView
etc. Because of it, I am using the .optional()
operator when combining reducers.
When dismissing from ThirdView
to FirstView
, I noticed a nondeterministic behavior, where sometimes the assertion in the .optional()
operator fails, and sometimes not.
I need help with figuring out what is causing the issue. Any hints on how to implement the "dismiss to root" action would be appreciated.
I am using Xcode 12.2 with Swift 5.3.1, iOS 14.2 and Swift Composable Architecure 0.9.0.
Here is the exact code that I am using (copy&paste in Xcode, and it should run):
import ComposableArchitecture
import SwiftUI
@main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
FirstView(store: Store(
initialState: FirstState(),
reducer: firstReducer,
environment: ()
))
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
// MARK: - First
struct FirstState: Equatable {
var second: SecondState?
}
enum FirstAction: Equatable {
case presentSecond
case dismissSecond
case second(SecondAction)
}
let firstReducer = Reducer<FirstState, FirstAction, Void>.combine(
secondReducer.optional().pullback(
state: \.second,
action: /FirstAction.second,
environment: { _ in () }
),
Reducer { state, action, _ in
switch action {
case .presentSecond:
state.second = SecondState()
return .none
case .dismissSecond:
state.second = nil
return .none
case .second(.third(.dismissToFirst)):
return .init(value: .dismissSecond)
case .second:
return .none
}
}
)
struct FirstViewState: Equatable {
let isPresentingSecond: Bool
init(state: FirstState) {
isPresentingSecond = state.second != nil
}
}
struct FirstView: View {
let store: Store<FirstState, FirstAction>
var body: some View {
WithViewStore(store.scope(state: FirstViewState.init(state:))) { viewStore in
VStack {
NavigationLink(
destination: IfLetStore(
store.scope(
state: \.second,
action: FirstAction.second
),
then: SecondView.init(store:)
),
isActive: viewStore.binding(
get: \.isPresentingSecond,
send: { $0 ? .presentSecond : .dismissSecond }
),
label: { Text("Present Second") }
)
}
.navigationTitle("First")
.navigationBarTitleDisplayMode(.inline)
}
}
}
// MARK: - Second
struct SecondState: Equatable {
var third: ThirdState?
}
enum SecondAction: Equatable {
case presentThird
case dismissThird
case third(ThirdAction)
}
let secondReducer = Reducer<SecondState, SecondAction, Void>.combine(
thirdReducer.optional().pullback(
state: \.third,
action: /SecondAction.third,
environment: { _ in () }
),
Reducer { state, action, _ in
switch action {
case .presentThird:
state.third = ThirdState()
return .none
case .dismissThird:
state.third = nil
return .none
case .third:
return .none
}
}
)
struct SecondViewState: Equatable {
let isPresentingThird: Bool
init(state: SecondState) {
isPresentingThird = state.third != nil
}
}
struct SecondView: View {
let store: Store<SecondState, SecondAction>
var body: some View {
WithViewStore(store.scope(state: SecondViewState.init(state:))) { viewStore in
VStack {
NavigationLink(
destination: IfLetStore(
store.scope(
state: \.third,
action: SecondAction.third
),
then: ThirdView.init(store:)
),
isActive: viewStore.binding(
get: \.isPresentingThird,
send: { $0 ? .presentThird : .dismissThird }
),
label: { Text("Present Third") }
)
}
.navigationTitle("Second")
.navigationBarTitleDisplayMode(.inline)
}
}
}
// MARK: - Third
struct ThirdState: Equatable {}
enum ThirdAction: Equatable {
case dismissToFirst
}
let thirdReducer = Reducer<ThirdState, ThirdAction, Void> { state, action, _ in
switch action {
case .dismissToFirst:
return .none
}
}
struct ThirdViewState: Equatable {
init(state: ThirdState) {}
}
struct ThirdView: View {
let store: Store<ThirdState, ThirdAction>
var body: some View {
WithViewStore(store.scope(state: ThirdViewState.init(state:))) { viewStore in
VStack {
Button(action: { viewStore.send(.dismissToFirst) }) {
Text("Dismiss to First")
}
}
.navigationTitle("Third")
.navigationBarTitleDisplayMode(.inline)
}
}
}