Abandoning and relaunching workflows in SwiftUI
Tyler-Keith-Thompson opened this issue · 4 comments
Alternate title: State and State Objects and Instances oh my!
Describe the bug
Your template says a clear and concise description of the problem is necessary but I cannot deliver on that, so here is a rambling and difficult explanation of the problem:
Using our sample app I am able to consistently reproduce an interesting bug. Essentially by chaining workflows I am able to make the profile screen disappear, LIKE MAGIC! The branch meetup
has this code reproduced. Short version: The profile screen has a workflow that isLaunched: .constant(true)
and it abandons that workflow to logout.
While that part works beautifully then you proceed through your workflow again and after you login the profile screen just vanishes.
Debugging suggested to me that a fix might be finding a way (somehow...) to make the workflow a @State
. I think that'll cause a host of other problems I don't want to think about, but I believe that is what needs to be done. This is more of a gut feeling but it's because instances of workflow just keep changing.
To Reproduce
Steps to reproduce the behavior:
- Go to profile
- Log in
- Log out
- Log in
- See error
Expected behavior
It should not do that.
Screenshots
Simulator.Screen.Recording.-.iPhone.12.Pro.Max.-.2021-07-29.at.09.39.54.mp4
Update: This has something to do with EnvironmentObjects. Minimally reproducible example:
struct ContentView: View {
@StateObject var user = User() // Object with a @Published property called `isLoggedIn` that is defaulted to false
var body: some View {
WorkflowLauncher(isLaunched: .constant(true))
.thenProceed(with: WorkflowItem(FR1.self))
.thenProceed(with: WorkflowItem(FR2.self))
.environmentObject(user)
}
}
struct FR1: View, FlowRepresentable {
weak var _workflowPointer: AnyFlowRepresentable?
var body: some View {
Button("Proceed") {
proceedInWorkflow()
}
}
}
struct FR2: View, FlowRepresentable {
weak var _workflowPointer: AnyFlowRepresentable?
@EnvironmentObject var user: User
var body: some View {
Button("Abandon") {
user.isLoggedIn = false
workflow?.abandon()
}
}
}
Hit some discoveries:
We have a branch, swiftui_abandon_bug
, where we are diagnosing the issue, and we have gotten it to a clearer scope of the issue. It seems that this is not just about abandon()
but the rerendering of WorkflowLauncher
when the State of ContentView
is updated.
Essentially you can change FR2
to take in the user as an ObservedObject
instead of using EnvironmentObject
and still see this fail. You can also split the steps of FR2 into 2 discrete steps to get better control of the failure.
REAL EOD:
We fixed it and another bug I haven't even filed yet around Spamming the proceed
and abandon
calls breaking down.
All the fixes have to do with adding @State
to the properties of ModifiedWorkflowView that we want to be retained between renderings. You can see this in the swiftui_abandon_bug
branch. I've made the change as well in main (I DIDN'T COMMIT!) to see if all the tests pass if the changes were made in isolation and the tests will pass.
I think next up for this Issue is to write up a test for the wrapped
property (this bug) and possibly one for the spamming of proceed
and abandon
. Then change them all to @State
and drop the mic. 🎤
REAL REAL EOD:
Unfortunately, none of the tests Richard suggested actually work with ViewInspector. Because of how it hosts and loads views, we cannot trigger the true SwiftUI lifecycle that causes the problem. In lieu of that, I've created a test using a somewhat unknown bitcast method that guarantees that ModifiedWorkflowView uses @State
properties for everything. It's less than ideal, but it does function, and it does fix all the problems we were seeing.