Swiftui Classes That Conforms Observableobject Should Be Singleton

SwiftUI Classes that conforms ObservableObject should be Singleton?

Why do you think a viewmodel should be a singleton? And especially, why should an ObservableObject conformant class need a singleton instance? That's a bad idea.

Not only is this absolutely unnecessary, this would also mean you cannot have several instances of the same view on the screen without them having shared state. This is especially bad on iPad if you want to support split screen and running 2 scenes of your app on the screen at the same time.

Don't make anything a singleton, unless you absolutely have to.

The only important thing to keep in mind with storing @ObservedObjects on SwiftUI Views is that they should never be initialised inside the view. When an @ObservedObject changes (or one of its @Published properties change), the View storing it will be reloaded. This means that if you create the object inside the View, whenever the object updates, the view itself will create a new instance of said object.

So this is a bad idea and won't work:

struct ContentView: View {
// Never do this
@ObservedObject private var vm = MyViewModel()

var body: some View {
Text(vm.result)
}
}

Instead, you need to inject the viewmodel into your View (by creating it in the parent view or in a coordinator, etc, wherever you create your ContentView from).

struct ParentView: View {
@State private var childVM = MyViewModel()

var body: some View {
ContentView(vm: childVM)
}
}

struct ContentView: View {
@ObservedObject private var vm: MyViewModel

// Proper way of injecting the view model
init(vm: MyViewModel) {
self.vm = vm
}

var body: some View {
Text(vm.result)
}
}

How to create a singleton that I can update in SwiftUI

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.

Why do I have to set @ObservedObject reference from my singleton to update data?

SwiftUI needs to know when something has changed in order to re-compute the body of the view that depends on the change.

With objects, it uses an @ObservedObject property wrapper (or @StateObject, or @EnvironmentObject), and the object has to conform to ObservableObject. When that object signals a change - either by updating its @Published property or by calling objectWillChange.send() directly - the SwiftUI knows to update the view.


So, in your example, when @ObservedObject var profile property "changes", body is recomputed, and by virtue of recomputing, reads UserProfile.sharedProfile and its properties.

How to make two class with the protocol of observable object access one another in swiftui

Here's one option, using onAppear to pass a reference to musicCheck:

class MusicController: ObservableObject {
@Published var isPlaying = false
var musicCheck : MusicCheck?

func setup(musicCheck : MusicCheck) {
self.musicCheck = musicCheck

//perform other setup logic
}
}

class MusicCheck: ObservableObject {
@Published var isPlayingAfter = false

init() {
// perform logic for when the application willEnterForeground
}
}

struct ContentView : View {
@StateObject private var musicCheck = MusicCheck()
@StateObject private var musicController = MusicController()

var body: some View {
VStack {
Text("Hello, world")
}.onAppear {
musicController.setup(musicCheck: musicCheck)
}
}
}

Another possibility is using a singleton pattern. I'd be cautious about this approach, as it can lead to situations that are challenging to test. Also, a mistake I see frequently is people creating a non-singleton instance and being confused about why they have multiple copies -- I've used a private init to try to avoid this.

class MusicController: ObservableObject {
@Published var isPlaying = false

init() {
MusicCheck.shared.someFunction()
}
}

class MusicCheck: ObservableObject {
static var shared = MusicCheck()

@Published var isPlayingAfter = false

private init() {
// perform logic for when the application willEnterForeground
}

func someFunction() {

}
}

struct ContentView : View {
@StateObject private var musicCheck = MusicCheck.shared
@StateObject private var musicController = MusicController()

var body: some View {
VStack {
Text("Hello, world")
}
}
}

ObservableObject text not updating even with objectWillChange - multiple classes

If you are using ObservableObject you don't need to write objectWillChange.send() in willSet of your Published properties.

Which means you can as well remove:

let objectWillChange = ObservableObjectPublisher()

which is provided by default in ObservableObject classes.

Also make sure that if you're updating your @Published properties you do it in the main queue (DispatchQueue.main). Asynchronous requests are usually performed in background queues and you may try to update your properties in the background which will not work.

You don't need to wrap all your code in DispatchQueue.main - just the part which updates the @Published property:

DispatchQueue.main.async {
self.humidity = ...
}

And make sure you create only one GetReadings instance and share it across your views. For that you can use an @EnvironmentObject.

In the SceneDelegate where you create your ContentView:

// create GetReadings only once here
let getReadings = GetReadings()

// pass it to WSManager
// ...

// pass it to your views
let contentView = ContentView().environmentObject(getReadings)

Then in your ReadingsView you can access it like this:

@EnvironmentObject var getReadings: GetReadings

Note that you don't need to create it in the TabView anymore:

TabView(selection: $selection) {
ReadingsView()
...
}

How to tell SwiftUI views to bind to nested ObservableObjects

Nested models does not work yet in SwiftUI, but you could do something like this

class SubModel: ObservableObject {
@Published var count = 0
}

class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()

var anyCancellable: AnyCancellable? = nil

init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}

Basically your AppModel catches the event from SubModel and send it further to the View.

Edit:

If you do not need SubModel to be class, then you could try something like this either:

struct SubModel{
var count = 0
}

class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()
}

Access @StateObject from function outside of view

Your timer is an instance variable but its closure is not a instance of the class and has no self.

You're going to have to do something to put "self" into the scope of the timer's block. One way to do that would be to create the timer in a member function of the instance:

private func makeTimer() -> Timer {
return Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true) { [weak self] _ in
if let empty = self?.watchlist.isEmpty,
empty == false {
}
}
}

When you call makeTimer() your code will be executing in the context of an instance. It will have access to a self.

Note that I have changed your block so that it capture's self "weakly" because the timer could exist beyond the life of the object so you have to be proactive against that possibility.

You could call makeTimer from your initializer.



Related Topics



Leave a reply



Submit