wwt/SwiftCurrent

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:

  1. Go to profile
  2. Log in
  3. Log out
  4. Log in
  5. 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.