Managing complex navigation flows #1042
Replies: 1 comment
-
You should probably solve it by adding a coordinating Reducer on top of this, and decouple A-E from each other so that they don't know anything about each other. Typically let an And create a wrapping OnboardingCoordinatorView with the corresponding view presentation code. This is the tricky part, at least for me. Currently I have only got forward navigation to work, without animation and with no possibility to go back. I will probably need to abandon using IfLetStores and using NavigationLink with isActive, but it was trickier than ai thought because the send action of the isActive binding seems to fire multiple times when back button is pressed. Maybe one has got to use custom toolbar with custom navigation back button... This is what I have now, in open source crypto wallet Zhip, branch rewriting the app from UIkit to SwiftUI + TCA public struct OnboardingState: Equatable {
public var step: Step
public var welcome: WelcomeState?
public var termsOfService: TermsOfServiceState?
public var setupWallet: SetupWalletState?
public var newPIN: NewPINState?
public init(
step: Step = .step0_Welcome,
welcome: WelcomeState? = nil,
termsOfService: TermsOfServiceState? = nil,
setupWallet: SetupWalletState? = nil,
newPIN: NewPINState? = nil
) {
self.step = step
self.welcome = .init()
self.termsOfService = termsOfService
self.setupWallet = setupWallet
self.newPIN = newPIN
}
}
public extension OnboardingState {
enum Step: Equatable {
case step0_Welcome
case step1_TermsOfService
case step2_SetupWallet
case step3_NewPIN
}
}
public enum OnboardingAction: Equatable {
case delegate(DelegateAction)
case welcome(WelcomeAction)
case termsOfService(TermsOfServiceAction)
case setupWallet(SetupWalletAction)
case newPIN(NewPINAction)
}
public extension OnboardingAction {
enum DelegateAction: Equatable {
case finishedOnboarding(wallet: Wallet, pin: Pincode?)
}
}
public struct OnboardingEnvironment {
public let backgroundQueue: AnySchedulerOf<DispatchQueue>
public var keychainClient: KeychainClient
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var passwordValidator: PasswordValidator
public var userDefaults: UserDefaultsClient
public var walletGenerator: WalletGenerator
public var walletRestorer: WalletRestorer
public init(
backgroundQueue: AnySchedulerOf<DispatchQueue>,
keychainClient: KeychainClient,
mainQueue: AnySchedulerOf<DispatchQueue>,
passwordValidator: PasswordValidator,
userDefaults: UserDefaultsClient,
walletGenerator: WalletGenerator,
walletRestorer: WalletRestorer
) {
self.backgroundQueue = backgroundQueue
self.keychainClient = keychainClient
self.mainQueue = mainQueue
self.passwordValidator = passwordValidator
self.userDefaults = userDefaults
self.walletGenerator = walletGenerator
self.walletRestorer = walletRestorer
}
}
public let onboardingReducer = Reducer<OnboardingState, OnboardingAction, OnboardingEnvironment>.combine(
welcomeReducer
.optional()
.pullback(
state: \.welcome,
action: /OnboardingAction.welcome,
environment: { _ in
WelcomeEnvironment()
}
),
termsOfServiceReducer
.optional()
.pullback(
state: \.termsOfService,
action: /OnboardingAction.termsOfService,
environment: { TermsOfServiceEnvironment(userDefaults: $0.userDefaults) }
),
setupWalletReducer
.optional()
.pullback(
state: \.setupWallet,
action: /OnboardingAction.setupWallet,
environment: {
SetupWalletEnvironment(
backgroundQueue: $0.backgroundQueue,
keychainClient: $0.keychainClient,
mainQueue: $0.mainQueue,
passwordValidator: $0.passwordValidator,
walletGenerator: $0.walletGenerator,
walletRestorer: $0.walletRestorer
)
}
),
newPINReducer
.optional()
.pullback(
state: \.newPIN,
action: /OnboardingAction.newPIN,
environment: {
NewPINEnvironment(
keychainClient: $0.keychainClient,
mainQueue: $0.mainQueue
)
}
),
Reducer { state, action, environment in
switch action {
case .welcome(.delegate(.getStarted)):
if environment.userDefaults.hasAcceptedTermsOfService {
state = .init(
step: .step2_SetupWallet,
setupWallet: .init()
)
} else {
state = .init(
step: .step1_TermsOfService,
termsOfService: .init(mode: .mandatoryToAcceptTermsAsPartOfOnboarding)
)
}
return .none
case .welcome(_):
return .none
case .termsOfService(.delegate(.didAcceptTermsOfService)):
assert(environment.userDefaults.hasAcceptedTermsOfService)
state.setupWallet = .init()
state.step = .step2_SetupWallet
return .none
case .termsOfService(.delegate(.done)):
assertionFailure("Done button should not be able to press when TermsOfService is presented from Onboarding.")
state.setupWallet = .init()
state.step = .step2_SetupWallet
return .none
case let .setupWallet(.delegate(.finishedSettingUpWallet(wallet))):
state.newPIN = .init(wallet: wallet)
state.step = .step3_NewPIN
return .none
case let .newPIN(.delegate(.skippedPIN(wallet))):
return Effect(value: .delegate(.finishedOnboarding(wallet: wallet, pin: nil)))
case let .newPIN(.delegate(.finishedSettingUpPIN(wallet, pin))):
return Effect(value: .delegate(.finishedOnboarding(wallet: wallet, pin: pin)))
default:
return .none
}
}
)
public struct OnboardingCoordinatorView: View {
let store: Store<OnboardingState, OnboardingAction>
public init(store: Store<OnboardingState, OnboardingAction>) {
self.store = store
}
}
public extension OnboardingCoordinatorView {
var body: some View {
WithViewStore(
store.scope(state: ViewState.init)
) { viewStore in
NavigationView {
Group {
switch viewStore.step {
case .step0_Welcome:
IfLetStore(
store.scope(
state: \.welcome,
action: OnboardingAction.welcome
),
then: WelcomeScreen.init(store:)
)
case .step1_TermsOfService:
IfLetStore(
store.scope(
state: \.termsOfService,
action: OnboardingAction.termsOfService
),
then: TermsOfServiceScreen.init(store:)
)
case .step2_SetupWallet:
IfLetStore(
store.scope(
state: \.setupWallet,
action: OnboardingAction.setupWallet
),
then: SetupWalletCoordinatorView.init(store:)
)
case .step3_NewPIN:
IfLetStore(
store.scope(
state: \.newPIN,
action: OnboardingAction.newPIN
),
then: NewPINCoordinatorView.init(store:)
)
}
}
}
}
}
}
private extension OnboardingCoordinatorView {
struct ViewState: Equatable {
let isSkipButtonVisible: Bool
let step: OnboardingState.Step
init(state: OnboardingState) {
let step = state.step
switch step {
case .step3_NewPIN:
self.isSkipButtonVisible = true
default:
self.isSkipButtonVisible = false
}
self.step = step
}
}
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
So I'm working on an app that has a fairly complex onboarding/connection flow and I'm wondering if I'm taking the right approach. As an example of the problem we're facing, let's say you have an app that has 5 screens; we'll call them screens
A, B, C, D, E
. Each screen is pushed on top of the previous and always in the same order. The problem is each screen may or may not be shown depending on what happens on the previous screens in the stack. So the navigation could beA -> B -> C -> D -> E
but alsoA -> B -> D
, orA -> E
, etc. Using the standard approachreducerA
would require reducersB, C, D, E
as sub reducers,reducerB
would require reducersC, D, E
and so on. It would also meanreducerA
can receive actions fromE
at the top level, but also nestedE
actions from every reducer in the chain that has also addedE
as a subreducer.We're using this library to alleviate some of those issues: https://github.com/johnpatrickmorgan/TCACoordinators, but I'd rather not rely on another 3rd party library. It also makes sending actions to and receiving actions from the subreducers more difficult. I was wondering if there's an approach with the vanilla architecture for these sort of issues.
Beta Was this translation helpful? Give feedback.
All reactions