lewis
lewis

Reputation: 3172

SwiftUI Parent view not updating after state change

I am working on a niche workout app. The flow is similar to the built in app, but a little more complicated.

I have the following code at the top level:

@main
struct WatchApp: App {
    
    @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate
    @StateObject var coordinator = SessionCoordinator()
    @StateObject var permissions = PermissionsViewModel()
    
    var body: some Scene {
        WindowGroup {
            NavigationView {
                switch coordinator.workout.state {
                // ✅ Works on load
                // ❌ Doesn't reload when user changes state back to `notStarted`

                case .notStarted:
                     StartView() 
                        .sheet(isPresented: $permissions.showPermissionsPrompt, content: {
                            PermissionsView(permissions: permissions)
                        })
                // ✅ Works
                case .countingDown:
                    CountdownView() 
                // ✅ Works    
                case .paddling, .paused:
                    WorkoutOrAlertView() 
                // ✅ Works    
                case .saving, .ended:
                    SummaryView()
                    
                }
            }
            .environmentObject(coordinator)
            
        }
    }
}

in the SummaryView there is a button which changes the state to notStarted coordinator.workout.state = .notStarted

This does NOT reload the start view as expected from the case statement.

The coordinator and the workout are ObservableObjects:

class SessionCoordinator: ObservableObject {
    
    @Published public var workout: WorkoutSession {
        didSet {
            DDLogVerbose("😵‍💫 workout changed")
        }
    }

class WorkoutSession: NSObject, ObservableObject {
    ....
    @Published var state = WorkoutSessionState.notStarted {
        didSet {
            DDLogVerbose("Workout state changed from: \(oldValue) to: \(state)")
        }
    }

So in summary, everything works as you would expect: ✅ Start a workout ✅ Show countdown if required ✅ Run workout ✅ End workout and get a summary ❌ Changing state to .notStarted does not reload StartView()

So my questions are:

Upvotes: 0

Views: 393

Answers (1)

jrturton
jrturton

Reputation: 119232

If you have a @Published property that holds a reference type, then this is only going to send objectWillChange when you set a new instance of the reference type. It doesn't matter if the property you changed is published - nothing in your code is listening to changes on that object.

If you genuinely want your coordinator to publish every time a property of the session changes, then you should manually observe and re-publish object will change. However, that is likely to lead to over-publishing and excessive re-rendering, particularly since this is a top-level object.

A better solution is to create a container view, which observes the session:

struct WorkoutView: View {
    @ObservedObject var workoutSession: WorkoutSession

    var body: some View {
        switch workoutSession.state {
        //... all your cases here
        }
    }
}

Then your original view would be:

NavigationView {
    WorkoutView(workoutSession: coordinator.workout) 
}

This means you'll get a new WorkoutView whenever the instance of the workout changes, and that workout view will monitor the state.

You'll need to either inject the permissions into the environment or pass that as an observed object as well so it can be picked up.

Oh - and using switch statements in a view like this? Fantastic. The best way!

Upvotes: 3

Related Questions