LAST CHANCE: Save 50% on all my Swift books and bundles! >>

Using @Observable and getting a "Accessing State's value outside of being installed..." error.

Forums > SwiftUI

I have a very simple 3/4 View thing.

For context, I am complete Swift/SwiftUI newb...

In my ContentView, I have an Observable written like this (I followed the examples here):

@Observable class SportMenuStatus {
    var isSportMenuShowing: Bool = false
    func toggleMenu() {
        isSportMenuShowing.toggle()
        print("toggleMenu running, \(isSportMenuShowing)")
    }
}

But that produces the error "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." when I try and interact with it in my other View which looks like this:

struct SportMenu: View {
    @Binding var isSportMenuShowing: SportMenuStatus
    @Binding var path: [String]

    var body: some View {
        VStack {
            ZStack {
                Image("sportmenu-no-cross")
                    .resizable()
                    .aspectRatio(contentMode: .fill)

                // Basketball slice
                VStack {
                    HStack {
                        Button("ioenienien") {
                            if path.last != "SportSplash" {
                                path.append("SportSplash")
                            }
                            isSportMenuShowing.toggleMenu()
                        }
                        .frame(width: 150, height: 40)
                        .background(Color.white)
                        .opacity(0.1)
                        Spacer()
                    }
                    Spacer()
                }.offset(x: 0, y: 417)

                // Close icon
                VStack {
                    HStack {
                        Spacer()
                        ZStack {
                            Image("close")
                            Button(action: {
                                    isSportMenuShowing.toggleMenu()
                            }) {
                                Color.white.opacity(0.1) // Transparent button
                            }
                            .frame(width: 50, height: 50)
                        }
                    }
                    Spacer()
                }
                .offset(x: -20, y: 60)
            }
        }
        .edgesIgnoringSafeArea(.top)
        .offset(y: isSportMenuShowing.isSportMenuShowing ? 0 : 800)
        .animation(.default, value: isSportMenuShowing.isSportMenuShowing)
    }
}

#Preview {
    SportMenu(isSportMenuShowing: ContentView().$isSportMenuShowing, path: .constant([""]))
}

When I press either button, that fires isSportMenuShowing.toggleMenu() in there after building, it produces that error.

Is it a case of somehow needing to initialise that var isSportMenuShowing: Bool = false somewhere else??

1      

But where do you instantiate your class? You have to make an instance of that first, then you can pass it around using binding, or you can inject it in environment and access it from there. But you have to have "the state of truth" of the object somewhere like so:

@State var sportMenuStatusObject = SportMenuStatus()

1      

@ygeras I have it instantiated in the view below, so my ContentView.swift starts like this:

@Observable class SportMenuStatus {
    var isSportMenuShowing: Bool = false
    func toggleMenu() {
        isSportMenuShowing.toggle()
        print("toggleMenu running, \(isSportMenuShowing)")
    }
}

struct ContentView: View {
    @State private var tab = 0
    @State var isSportMenuShowing = SportMenuStatus()
    var body: some View {
            VStack {
                ZStack {

I thought the second line in ContentView was doing that?

1      

What about this? @Binding var path: [String]. Where do you instantiate it? With these small snippets of code it's rather difficult to see the whole picture.

1      

OK, here is the complete contents I have. I have been reading and watching more things on this and I clearly don't understand this conceptually.

I am only targeting iOS17+ so trying to use the new @Observable, @State and @Environment. From what I have read and watched, I believe that if I set a @Observable in my root ContentView, I can then access and amend the properties of that view, across different files, without needing to pass it in to each view. Is that correct? In the example code below, I want a property I can toggle, throughout my various views, that will bring in a menu view.

So in my ContentView, I am setting an Observable:

import SwiftUI

@Observable class SportMenuStatus {
    var isSportMenuShowing = false
}

struct ContentView: View {
    @State private var tab = 0
    var body: some View {
            VStack {
                ZStack {
                    TabView(selection: $tab) {
                        Group {
                            Home()
                                .tabItem {
                                    Image(tab == 0 ? "nav-home-selected" : "nav-home")
                                    Text("Home")
                                }
                                .tag(0)

                        }
                        .toolbarBackground(.ultraThinMaterial, for: .tabBar)
                        .toolbarBackground(.visible, for: .tabBar)
                        .toolbarColorScheme(.dark, for: .tabBar)
                    }

                }
            }.edgesIgnoringSafeArea(.top)
        }
}

#Preview {
    ContentView()
}

That view then loads Home, which lives in Home.swift, which contains a button that SHOULD toggle that isSportMenuShowing value in the observable but doesn't. Well, at least the value in that print statement doesn't change:

import SwiftUI

struct Home: View {
    @State private var stackPath: [String] = []

    var body: some View {
        NavigationStack(path: $stackPath) {
            VStack {
                ZStack {
                    Image("home")
                        .resizable()
                        .aspectRatio(contentMode: .fill)

                    // The Menu button
                    HStack {
                        VStack {
                            Group {
                                Button(action: {
                                    SportMenuStatus().isSportMenuShowing.toggle()
                                    print("clicks,\(SportMenuStatus().isSportMenuShowing)")
                                }) {
                                    Color(.white)
                                }.frame(width: 100, height: 45)

                                    .opacity(0.1)
                            }
                            Spacer()
                        }
                        Spacer()
                    }
                    .offset(x: 5, y: 55)
                    SportMenu(path: $stackPath)
                        .zIndex(10.0)
                }
                Spacer()
            }
            .navigationDestination(for: String.self) { route in
                switch route {
                case "SportSplash":
                    SportSplash(path: $stackPath)
                        .navigationBarBackButtonHidden(true)
                default:
                    EmptyView()
                }
            }
        }
        .onChange(of: stackPath) { oldValue, newValue in
            print(newValue)
        }

    }
}

#Preview {
    Home()
}

So, am I misunderstanding how I use these @Observable work?

Ultimately, I just want to be able to get and set that Bool from multiple files and have the views update on the basis of whether it is true/false.

1      

But wait you have this in your Home view in button action...

SportMenuStatus().isSportMenuShowing.toggle()

You're basically creating a new object which may change every single time the view is refreshed. And it can happen many many times. Your task is to create this object let's say in Content view and pass it to subviews either using @Bindable <- as this is used for observable objects, @Binding is used for simple types as Int, String, Bool etc. or via Environment, which allows you to access this object from any subviews without passing in each subview your object.

In your Home view try instead of instantiating a new object, pass in that object from the place you have created it.

1      

OK, I have this working now, only problem is that all Previews apart from ContentView crash. I don't see any errors but I am unsure if I need to be passing something into the Preview?

Here is what I did to get this working:

ContentView.swift

@Observable class SportMenuStatus {
    var isSportMenuShowing = false
}

struct ContentView: View {

    // Initialise the menu state
    @State var menuState = SportMenuStatus()
    @State private var tab = 0

    var body: some View {
            VStack {
                ZStack {
                    TabView(selection: $tab) {
                        Group {
                            Home()
                                .tabItem {
                                    Image(tab == 0 ? "nav-home-selected" : "nav-home")
                                    Text("Home")
                                }
                                .tag(0)

                        }
                        .toolbarBackground(.ultraThinMaterial, for: .tabBar)
                        .toolbarBackground(.visible, for: .tabBar)
                        .toolbarColorScheme(.dark, for: .tabBar)
                    }

                }
            }.edgesIgnoringSafeArea(.top)
            .environment(menuState)
        }

}

#Preview {
    ContentView()
}

I have used the environment() modifier to pass that in. Then in subsequent views, such as Home.swift, I am using that observable property like this:

struct Home: View {
    @State private var stackPath: [String] = []
    @Environment(SportMenuStatus.self) var menuState

    var body: some View {
        NavigationStack(path: $stackPath) {
            VStack {
                ZStack {
                    Image("home")
                        .resizable()
                        .aspectRatio(contentMode: .fill)

                    // The Menu button
                    HStack {
                        VStack {
                            Group {
                                Button(action: {
                                    menuState.isSportMenuShowing.toggle()
                                    print("clicks,\(menuState.isSportMenuShowing)")
                                }) {
                                    Color(.white)
                                }.frame(width: 100, height: 45)
                                .opacity(0.1)
                            }
                            Spacer()
                        }
                        Spacer()
                    }
                    .offset(x: 5, y: 55)
                    SportMenu(path: $stackPath)
                        .zIndex(10.0)
                }
                Spacer()
            }
            .navigationDestination(for: String.self) { route in
                switch route {
                case "SportSplash":
                    SportSplash(path: $stackPath)
                        .navigationBarBackButtonHidden(true)
                default:
                    EmptyView()
                }
            }
        }
        .onChange(of: stackPath) { oldValue, newValue in
            print(newValue)
        }
    }
}

#Preview {
    Home()
}

It seems to work as expected. Does this seem 'correct'? Or is there something I am doing there that would obviously be messing up the previews?

1      

#Preview is a separate struct and has no idea about your current environment.

As far as I can recall you just need to add environment modifier to your view in Preview and instantiate your object there directly.

So this should do the job.

#Preview {
    Home()
      .environment(SportMenuStatus())
}

1      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.