3

I want to create a global variable for showing a loadingView. I tried lots of different ways but could not figure out how to. I need to be able to access this variable across the entire application and update the MotherView file when I change the boolean for the singleton.

struct MotherView: View {
    
    @StateObject var viewRouter = ViewRouter()
        
    var body: some View {
        
        if isLoading { //isLoading needs to be on a singleton instance
            Loading()
        }

        
        switch viewRouter.currentPage {
        case .page1:
            ContentView()
        case .page2:
            PostList()
        }
    }
}

struct MotherView_Previews: PreviewProvider {
    static var previews: some View {
        MotherView(viewRouter: ViewRouter())
    }
}

I have tried the below singleton but it does not let me update the shared instance? How do I update a singleton instance?

struct LoadingSingleton {
    static let shared = LoadingSingleton()
    var isLoading = false

    private init() { }
}
HangarRash
  • 7,314
  • 5
  • 5
  • 32
Justin Comstock
  • 175
  • 1
  • 13

2 Answers2

10

Make your singleton a ObservableObject with @Published properties:

struct ContentView: View {
    @StateObject var loading = LoadingSingleton.shared
    
    var body: some View {
        if loading.isLoading {
            Text("Loading...")
        }
        ChildView()
        Button(action: { loading.isLoading.toggle() }) {
            Text("Toggle loading")
        }
    }
}

struct ChildView : View {
    @StateObject var loading = LoadingSingleton.shared

    var body: some View {
        if loading.isLoading {
            Text("Child is loading")
        }
    }
}

class LoadingSingleton : ObservableObject {
    static let shared = LoadingSingleton()
    @Published var isLoading = false
    
    private init() { }
}

I should mention that in SwiftUI, it's common to use .environmentObject to pass a dependency through the view hierarchy rather than using a singleton -- it might be worth looking into.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Is there anyway to bypass needing to use @ StateObject and @ ObservedObject? That was my goal with the singleton route. @jnpdx – Justin Comstock May 07 '21 at 18:48
  • Not really... I mean, you could try to make a `Combine` version all with Publishers, but it seems like an uphill battle and you'll end up having to add a ton of boilerplate code to every view. `@StateObject` and `@ObservedObject` are property wrappers that help tell SwiftUI to update the view when something changes. Otherwise, the `View` doesn't know to update. Is there a reason that you're trying to avoid using them? – jnpdx May 07 '21 at 18:48
  • @jnpdx how to use . environmentObject in the above context? Im updating isLoading variable in some ViewModel and trying to use it in another swiftUI file. But the values dont update – Honey Oct 20 '21 at 06:18
  • @Honey hard to say what's going wrong without seeing a [mre] in a question – jnpdx Oct 20 '21 at 17:07
  • @jnpdx here is my question https://stackoverflow.com/questions/69628795/environmentobject-property-not-working-properly-in-swiftui/69629171?noredirect=1#comment123088101_69629171 . Plz suggest – Honey Oct 21 '21 at 05:29
  • Neat and simple solution! – Yash Apr 18 '23 at 03:21
1

First, make LoadingSingleton a class that adheres to the ObservableObject protocol. Use the @Published property wrapper on isLoading so that your SwiftUI views update when it's changed.

class LoadingSingleton: ObservableObject {
    @Published var isLoading = false
}

Then, put LoadingSingleton in your SceneDelegate and hook it into your SwiftUI views via environmentObject():

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    static let singleton = LoadingSingleton()

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = ContentView()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(SceneDelegate.singleton))
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

To enable your SwiftUI views to update when changing isLoading, declare a variable in the view's struct, like this:

struct MyView: View {
    @EnvironmentObject var singleton: LoadingSingleton

    var body: some View {
        //Do something with singleton.isLoading
    }
}

When you want to change the value of isLoading, just access it via SceneDelegate.singleton.isLoading, or, inside a SwiftUI view, via singleton.isLoading.

West1
  • 1,430
  • 16
  • 27