0

I'm curious, is there a way to implement a singleton @ObservedObject? I'm trying to find an alternative to the environmentObject pattern in SwiftUI, and for some situations (eg for an AppState object where there should only be a single instance), I'm experimenting with the idea of using a single shared instance that can be picked up directly in subviews, without any setup from the parents.

This would have the delightful side benefit of easier previews (since the need for .environmentObject(...) would go away).

Here's some sample code I've been playing with:

// An object to hold App State. There should only ever be one instance.
class AppState : ObservableObject {
    @Published var showLabels = false
    
    static var shared: AppState { AppState() }
}

// A subview that could pick up the AppState through static member on the class
struct IdentityPanel: View {
    @ObservedObject var appState = AppState.shared
    @State private var name = ""
    
    var body: some View {
        Section("Identity") {
            if appState.showLabels {
                Text("Enter your Name here:")
            }
            TextField("Name", text: $name)
        }
    }
}

// The parent view, it also accesses the AppState directly, but doesn't
// do any setup for the child.
struct SingletonAppState: View {
    @ObservedObject var appState = AppState.shared

    var body: some View {
        Form {
            Toggle("Show Labels", isOn: $appState.showLabels)
            
            IdentityPanel()
        }
    }
}

// Look, Ma, no .environmentObject!
struct SingletonAppState_Previews: PreviewProvider {
    static var previews: some View {
        SingletonAppState()
    }
}

Now this compiles and everything, but value changes aren't being picked up correctly. Changing the showLabels value in the parent doesn't cause the child to refresh, as I had hoped.

My first thought was that maybe the AppState object needs to be instantiated in a view with a @StateObject decorator, to have all the required scaffolding set up. So I tried this newer definition for the class:

class AppState : ObservableObject {
    @Published var showLabels = false
        
    private struct StateHolder : View {
        @StateObject var state = AppState()
        var body: some View {
            Text("_")
        }
    }
    static private var _stateHolder: StateHolder { StateHolder() }
    static var shared: AppState { _stateHolder.state }
}

But that didn't work either. So then I thought that maybe the owner of the State needs to be in the view hierarchy directly, so I changed the decorator in SingletonAppState from @ObservedObject to @StateObject. But that didn't improve things either.

I don't understand what's going on at a lower level to make any more progress. Can someone have any insight in this, please? Hopefully, I've just missed something simple.

Curious Jorge
  • 354
  • 1
  • 9
  • 2
    Your `shared` is creating a *new* instance each time. See https://stackoverflow.com/questions/67439842/how-to-create-a-singleton-that-i-can-update-in-swiftui – jnpdx Jan 03 '23 at 19:48
  • Well wouldn't you know it? Changing it to `static let shared = AppState() // var works too` took care of things. Thanks a million! – Curious Jorge Jan 03 '23 at 20:03
  • I have, and on the other page, too. But I'm confused about where I went wrong. Why was my version not a singleton? I thought it was a lazily computed [Type Property](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID264), which should still be a singleton, no? – Curious Jorge Jan 03 '23 at 20:14
  • Without the `{ }` it would be -- `static` variables are `lazy` by default. With the `{ }` it becomes a static computed property. – jnpdx Jan 03 '23 at 20:16
  • I knew that part. :-) But I thought it would be computed only once and stored and retrieved using the usual lazy mechanism. I have to say, that's terribly disappointing. Re-reading the docs very carefully, I can see that it didn't say that those computed Type proper are computed lazily only once. But it sure is a natural conclusion to draw. Especially since this is the pattern that the Xcode generated CoreData project uses for the `preview` type property on `PersistenceController`. And that doesn't seem like something you'd want to recompute. – Curious Jorge Jan 03 '23 at 21:14
  • Oh, actually, scratch that. Boy, there are some subtle gotchas in swift. The following is what I was trying to do: `static var shared = { AppState() }()` **This** is what the CoreData project does, and that is in fact, a lazy type property that is computed only once. – Curious Jorge Jan 03 '23 at 21:23

0 Answers0