1

Warning, this is a specific case with Environment variables. I don't say that all @StateObjects should be Singleton (like ViewModels)! It would be wrong in my point of view.

Here is the case: In a SwiftUI iOS app, I use an ObservableObject class for settings, with @Published properties:

final class Settings: ObservableObject {
   @Published var disableLockScreen = false // Just a dummy example
}

In the @main App struct, I initialize it with @StateObject and inject it in my views hierarchy:

@main
struct MyTestApp: App {
   @StateObject private var settings = Settings()

   var body: some Scene {
      MainView()
         .environmentObject(settings)
   }
}

So MyTestApp owns the instance of Settings and all child Views can access it with @EnvironmentObject var settings: Settings.
OK... This is really convenient and great for Views. But...

I don't need to only access this Settings class in Views. I also need it in ViewModels that are not View structs but ObservableObject classes. In a ViewModel, I maybe want to access a property from Settings in code part (that are not directly linked to a View). For example, I have something like UIApplication.shared.isIdleTimerDisabled = settings.disableLockScreen.

To let my ViewModels access Settings, I have to inject it in their init. Like explained here or here. It is possible, of course, but to be honest, it generates a lot of code for not much and it becomes a lot more complicated to read.

So I found this accepted answer that shows an example of Singleton stored into a @StateObject. I was then thinking about changing my Settings class to be a Singleton. In MyTestApp, I only change Settings() with Settings.shared to store the Singleton instance in the App and continue to provide it to child views:

@main
struct MyTestApp: App {
   @StateObject private var settings = Settings.shared

   var body: some Scene {
      MainView()
         .environmentObject(settings)
   }
}

Where it really becomes better, it is in ViewModels: They now can simply access Settings.shared instance if they need access to its property. No need of dependency injection with lot of code.

In my point of view, it is really simple, smooth and correct. But I am not sure at all. Is using a Singleton as an @StateObject that is shared in the environment a good approach? Is there something I don't see that may cause issues? Should all @StateObjects shared into environment and used by other than Views should be Singletons?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Jonathan
  • 606
  • 5
  • 19
  • Singletons are generally considered bad practice/code smell, there are many articles on the subject as to why. They are generally used by beginners because it is "easy" but it is not scalable. But this is an opinion based question any engineer can argue the benefits and disadvantages. Pick what works for you, your coding style will evolve with time. DI is scalable, with a little code in the beginning you can swap services such as for mocking with single lines of code. – lorem ipsum Nov 07 '22 at 13:16
  • I totally agree and I am not a beginner programmer myself... But SwiftUI is quite new. In that specific context, I don't see why Singleton won't be a nice implementation. I find it pretty smooth in that scenario. So my question really needs an answer and not "do what is best for you". If Singleton is not a good idea in this case, why? You know what I mean? – Jonathan Nov 07 '22 at 14:18
  • Why is opinion based and out of scope for SO. I personally think it is an oxymoron to use a singleton with a `StateObject` and like I said before it is an unscalable choice. But if like you said you are not a beginner you should be able to argue both ways. There is no right or wrong answer for this. Why not use some kind of architecture, Settings generally require storage, you can just as easily access the settings from anywhere with any other architecture pattern. – lorem ipsum Nov 07 '22 at 14:26
  • You're right and that's exactly why I've decided to ask a question here: I argue both ways with myself since hours and can't find a good reason to implement or not this @StateObject + Singleton. ^^ So I wanted to ask if there is a really bad reason to not do that (it would make my choice easier). – Jonathan Nov 07 '22 at 14:31
  • it is contradictory and unscalable – lorem ipsum Nov 07 '22 at 14:36

1 Answers1

0

Just think of @StateObject as @State when you need your view state to be a reference type object, which should not be very often now that we have .task. Also, you wouldn't normally use @StateObject in the App struct because then the body is needlessly recomputed on every change when there is nothing using its properties. State (in both cases) declares a state of truth, you never pass arguments that can change into their inits because they have no way to watch for changes.

For your Settings, if it is just some simple vars then that can be a struct and you can use a custom EnvironmentKey to make it available everywhere. If you need to access settings in custom structs (that are declated as @State var customStruct = CustomStruct) then make those conform to DynamicProperty and access it in the func update(). update is called if the @Environment changes, the same way body is called on a View. You can move any other related @State or @StateObject into the DynamicProperty. e.g.

struct CustomStruct: DynamicProperty {
    @Environment(\.settings) var settings
    @State var ...
    @StateObject var ...

    func update() {
        // environment or state values changed
    }
}

If Settings is holding model data and you need a reference type, e.g. you are doing some async loading or saving then you can use a singleton and pass it down as environmentObject, e.g.

@main
struct MyTestApp: App {
   var body: some Scene {
      MainView()
         .environmentObject(Settings.shared)
   }
}

And in previews

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
.environmentObject(Settings.preview)
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
  • In my case, Settings class is more than just simple vars (I took Settings as an example, but I have other ObservableObjects such as my model). If I don't store them in App with @StateObject but only pass them down with .environmentObject(Settings.shared), who own them? Is it same as using @StateObject? The environment will? – Jonathan Nov 07 '22 at 19:23