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 @ObservedObject
s on SwiftUI View
s 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
Make a Grid of Buttons of Same Width and Height in Swiftui
Formula to Pick Every Pixel in a Bitmap Without Repeating
Generic Method Override Not Working in Swift
Swiftui Share Sheet Crashes iPad
How to Sort Dates in a Dictionary
How to Apply a Grace Time Using Rx
How to Use Nsvisualeffectview to Blend Window with Background
Swift- How to Display Image Over Button
How to Skip Iterations of a For-In Loop (Swift 3)
Modifying an Array Passed as an Argument to a Function in Swift
Selectively Remove and Delete Objects from a Nsmutablearray in Swift
Showing Notification Banner on MAC with Swift
Classes in Swift Files Inside Folder References Not Seen by Xcode 10's Compiler
How to Cast [Int8] to [Uint8] in Swift
How to Make a Custom Geometryreader in Swiftui