Search code examples
swiftswiftuistatewatchostabview

WatchOS Using ObservableObject in Conditional in View Causing Runtime Error


I use an ObservableObject to keep the state of whether a user is subscribed to my app or not, and based on the subscription status, show different views. This worked fine prior to Xcode 13 and WatchOS 8, but now this is causing a runtime error of runtime: SwiftUI: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. And, the binding does not update per the error. This occurs on both Xcode 13.1 and 13.2b2

This code below reproduces the error:

struct MultiPageView: View {
    @ObservedObject var subscribed = SubscribedModel.shared
    
    var body: some View {
        if subscribed.value {
            TabView {
                ViewOne()
                ViewTwo()
                ViewThree()
                ToggleView()
            }
            .tabViewStyle(PageTabViewStyle())
        } else {
            TabView {
                ViewOne()
                ToggleView()
            }
            .tabViewStyle(PageTabViewStyle())
        }
    }
}

struct ToggleView: View {
    @ObservedObject var subscribed = SubscribedModel()

    var body: some View {
        Toggle(isOn: $subscribed.value) {
            Text("Subscribed")
        }
    }
}

class SubscribedModel: ObservableObject {
    public static let shared = SubscribedModel.shared
    
    @Published var value: Bool = false
}

I am only listing ViewOne for brevity, but ViewTwo and ViewThree are the same with different text:

struct ViewOne: View {
    var body: some View {
        Text("View One")
            .padding()
    }
}

If you navigate to the ToggleView(), and switch the toggle, the error pops immediately. Any suggestions to fix this?

Update per @LoremIpsum comment:

struct MultiPageView: View {
    @StateObject var subscribed = SubscribedModel()
    
    var body: some View {
        if subscribed.value {
            TabView {
                ViewOne()
                ViewTwo()
                ViewThree()
                ToggleView(subscribed: $subscribed.value)
            }
            .tabViewStyle(PageTabViewStyle())
        } else {
            TabView {
                ViewOne()
                ToggleView(subscribed: $subscribed.value)
            }
            .tabViewStyle(PageTabViewStyle())
        }
    }
}

struct ToggleView: View {
    @Binding var subscribed: Bool

    var body: some View {
        Toggle(isOn: $subscribed) {
            Text("Subscribed")
        }
    }
}

It is now switching between the TabViews, but the error still remains, and is showing up immediately. Deleted DerivedData and cleaned build folder. Any thoughts?

I will add that this same basic code is running fine on iOS 15. It is just WatchOS that is popping the error.


Solution

  • I was having the same issue for a long time, and this is still happening on Xcode 13.2.1.

    Seems to be an issue with TabView on watchOS, because if you replace the TabView for another View the error is gone.

    The solution is to use the initialiser for TabView with a selection value: init(selection:content:)

    1 Define a property for selection

    @State private var selection = 0
    

    2 Update TabView

    From

    TabView {
        // content
    }
    

    To

    TabView(selection: $selection) {
        // content
    }
    

    Updating your code would look like this:

    struct MultiPageView: View {
        @StateObject var subscribed = SubscribedModel()
        @State private var selection = 0
        
        var body: some View {
            if subscribed.value {
                TabView(selection: $selection) {
                    ViewOne()
                    ViewTwo()
                    ViewThree()
                    ToggleView(subscribed: $subscribed.value)
                }
                .tabViewStyle(PageTabViewStyle())
            } else {
                TabView(selection: $selection) {
                    ViewOne()
                    ToggleView(subscribed: $subscribed.value)
                }
                .tabViewStyle(PageTabViewStyle())
            }
        }
    }
    

    Basically just defining a @State property for TabView.selection, and using it on both your TabViews (using separated properties would also work).