Having a SwiftUI project generated by Xcode, and adding a custom MyView with a MyViewModel.
The ContentView just renders MyView.
The problem:
When the ContentView gets reloaded (the reload button changes its state), MyViewModel gets somehow disconnected from MyView (the MyView counter stops incrementing in the UI when the button is clicked), but the console logs show the incrementation works.
If the model subscribes to a publisher, it does not get unsubscribed because the instance is not released. Therefore, the instances still process incoming messages and alter the app's data and state.
Looking at the instance counters and memory addresses in the console:
Every time the ContentView gets refreshed, a new MyView and MyViewModel instances get created. However, the counter incrementation uses the original first-created model instance.
Some model instances did not get released.
EDIT: The model needs to be recreated every time MyView is recreated.
import SwiftUI
struct ContentView: View {
#State
private var reloadCounter = 0
var body: some View {
VStack {
Button(action: { self.reloadCounter += 1 },
label: { Text("Reload view") })
Text("Reload counter: \(reloadCounter)")
MyView().environmentObject(MyViewModel())
}
}
}
import SwiftUI
struct MyView: View {
#EnvironmentObject
private var model: MyViewModel
var body: some View {
VStack {
Button(action: { self.model.counter += 1 },
label: { Text("Increment counter") })
Text("Counter value: \(model.counter)")
}
.frame(width: 480, height: 300)
}
init() { withUnsafePointer(to: self) { print("Initialising MyView struct instance \(String(format: "%p", $0))") }}
}
import Combine
class MyViewModel: ObservableObject {
private static var instanceCount: Int = 0 { didSet {
print("SettingsViewModel: \(instanceCount) instances")
}}
#Published
var counter: Int = 0 { didSet {
print("Model counter: \(counter), self: \(Unmanaged.passUnretained(self).toOpaque())")
}}
init() { print("Initialising MyViewModel class instance \(Unmanaged.passUnretained(self).toOpaque())"); Self.instanceCount += 1 }
deinit { print("Deinitialising MyViewModel class instance \(Unmanaged.passUnretained(self).toOpaque())"); Self.instanceCount -= 1 }
}
Any clue what did I do wrong?
The image below depicts the app's UI after all the actions in the logs were performed.
You create new view model on every refresh, so just move it outside body, like
struct ContentView: View {
#State
private var reloadCounter = 0
private let vm = MyViewModel() // << here !!
var body: some View {
VStack {
Button(action: { self.reloadCounter += 1 },
label: { Text("Reload view") })
Text("Reload counter: \(reloadCounter)")
MyView().environmentObject(vm) // << reference only !!
}
}
}
Note: if you don't need it deeply in sub-view hierarchy then consider to use #ObservedObject instead of #EnvironmentObject and pass reference via constructor (because environment object is stored somewhere outside and you have less control over its life-cycle)
Related
If I have an ObservableObject like...
class Foo: ObservableObject {
#Published var value: Int = 1
func update() {
value = 1
}
}
And then a view like...
struct BarView: View {
#ObservedObject var foo: Foo
var body: some View {
Text("\(foo.value)")
.onAppear { foo.update() }
}
}
Does this cause the view to constantly refresh? Or does SwiftUI do something akin to removeDuplicates in the subscribers that it creates?
I imagine the latter but I've been struggling to find any documentation on this.
onAppear is called when the view is first brought on screen. It's not called again when the view is refreshed because a published property has updated, so your code here would just bump the value once, and update the view.
If you added something inside the body of view that updated the object, that would probably trigger some sort of exception, which I now want to try.
OK, this:
class Huh: ObservableObject {
#Published var value = 1
func update() {
value += 1
}
}
struct TestView: View {
#StateObject var huh = Huh()
var body: some View {
huh.update()
return VStack {
Text("\(huh.value)")
}.onAppear(perform: {
huh.update()
})
}
}
Just puts SwiftUI into an infinite loop. If I hadn't just bought a new Mac, it would have crashed by now :D
When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.
When I have a nested ObservedObject, changes in a published property of a nested object do not updated the UI until something happens to the parent object. Is this a feature, a bug (in SwiftUI) or a bug in my code?
Here is a simplified example. Clicking the On/Off button for the parent immediately updates the UI, but clicking the On/Off button for the child does not update until the parent is updated.
I am running Xcode 12.5.1.
import SwiftUI
class NestedObject: NSObject, ObservableObject {
#Published var flag = false
}
class StateObject: NSObject, ObservableObject {
#Published var flag = false
#Published var nestedState = NestedObject()
}
struct ContentView: View {
#ObservedObject var state = StateObject()
var body: some View {
VStack {
HStack {
Text("Parent:")
Button(action: {
state.flag.toggle()
}, label: {
Text(state.flag ? "On" : "Off")
})
}
HStack {
Text("Child:")
Button(action: {
state.nestedState.flag.toggle()
}, label: {
Text(state.nestedState.flag ? "On" : "Off")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#ObservedObject or #StateObject updates the view when the ObservableObject updates. This happens when a #Published property is changed or when you directly call objectWillChange.send().
So, the "normal" (and the simplest) approach is to use a value type, e.g. a struct, for a #Published property.
struct NestedObject {
var flag = false
}
The reason this works, is that the entire NestedObject changes when its properties are modified, because struct is a value-type. In contrast, a reference-type class doesn't change (i.e. reference remains the same) when its property is modified.
But, sometimes you might need it to be a reference-type object, because it might have its own life cycle, etc...
In that case, you could definitely just call state.objectWillChange.send(), but that would only work if the view initiates the change, not when the nested object initiates the change. The best general approach here, in my opinion, is to use a nested inner view that has its own #ObservedObject to observe changes of the inner object:
struct ContentView: View {
private struct InnerView: View {
#ObservedObject var model: NestedObject
var body: some View {
Text("Child:")
Button(action: {
model.flag.toggle()
}, label: {
Text(model.flag ? "On" : "Off")
})
}
}
#StateObject var state = OuterObject() // see comments 1, 2 below
var body: some View {
VStack {
HStack {
Text("Parent:")
Button(action: {
state.flag.toggle()
}, label: {
Text(state.flag ? "On" : "Off")
})
}
HStack {
InnerView(model: state.nestedObject)
}
}
}
}
1. You shouldn't call your class StateObject, since it clashes with the StateObject property wrapper of SwiftUI. I renamed it to OuterObject.
2. Also, you should use #StateObject instead of #ObservedObject if you instantiate the object inside the view.
It works as supposed: ObservableObject only sends a notification about the changes of #Published properties but doesn't propagate change notification of nested ObservableObjects.
I'd follow the advice from New Dev: use a struct when you can or use a separate View to subscribe to the nested object.
If you truly need nested ObservableObjects, you can propagate the objectWillChange event from the nested object to the outer object using Combine:
import Combine
import SwiftUI
class InnerObject: ObservableObject {
#Published var flag = false
}
class OuterObject: ObservableObject {
#Published var flag = false
var innerObject = InnerObject() {
didSet {
subscribeToInnerObject()
}
}
init() {
subscribeToInnerObject()
}
private var innerObjectSubscription: AnyCancellable?
private func subscribeToInnerObject() {
// subscribe to the inner object and propagate the objectWillChange notification if it changes
innerObjectSubscription = innerObject.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
struct ContentView: View {
#ObservedObject var state = OuterObject()
var body: some View {
VStack {
Toggle("Parent \(state.flag ? "On" : "Off")", isOn: $state.flag)
Toggle("Child \(state.innerObject.flag ? "On" : "Off")", isOn: $state.innerObject.flag)
}
.padding()
}
}
Thank you for clarifications. The most valuable take-away for me is the fact that this behavior is not a bug (of SwiftUI), but is a by-design behavior.
SwiftUI (more precisely, Combine) see changes only in values, therefore, it can see changes in the property value changes of #Published struct instances, but not #Published class instances.
Therefore, the answer is "use struct instances for the nested objects if you want to update the UI based on the changes in the property values of those nested objects. If you have to use class instances, use another mechanism to explicitly notify changes".
Here is the modified code using struct for NestedObject instead of class.
import SwiftUI
struct NestedObject {
var flag = false
}
class OuterObject: NSObject, ObservableObject {
#Published var flag = false
#Published var nestedState = NestedObject()
}
struct ContentView: View {
#ObservedObject var state = OuterObject()
var body: some View {
VStack {
HStack {
Text("Parent:")
Button(action: {
state.flag.toggle()
}, label: {
Text(state.flag ? "On" : "Off")
})
}
HStack {
Text("Child:")
Button(action: {
state.nestedState.flag.toggle()
}, label: {
Text(state.nestedState.flag ? "On" : "Off")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
try this, works well for me on ios-15, catalyst 15, macos 12, using xcode 13:
HStack {
Text("Child:")
Button(action: {
state.objectWillChange.send() // <--- here
state.nestedState.flag.toggle()
}, label: {
Text(state.nestedState.flag ? "On" : "Off")
})
}
When debugging an issue with an app I am working on, I managed to shrink it down to this minimal example:
class RadioModel: ObservableObject {
#Published var selected: Int = 0
}
struct RadioButton: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel()
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton(idx: 0)
RadioButton(idx: 1)
RadioButton(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest()
}
}
}
}
This code looks pretty reasonable to me, although it exhibit a peculiar bug: when I hit the Refresh button, the radio buttons stop working. The radio buttons are not refreshed, and keep a reference to the old RadioModel instance, so when I click them they update that, and not the new one created after Refresh causes a new RadioListTest to be constructed. I suspect there is something wrong in the way I use EnvironmentObjects but I didn't find any reference suggesting that what I am doing is wrong. I know I could fix this particular problem in various ways that force a refresh in the radio buttons, but I would like to be able to understand which cases require a refresh forcing hack, I can't sprinkle the code with these just because "better safe than sorry", the performance is going to be hell if I have to redraw everything every time I make a modification.
edit: a clarification. The thing that is weird in my opinion and for which I would want an explanation, is this: why on refresh the RadioListTest is re-created (together with a new RadioModel) and its body re-evaluated but RadioButtons are created and the body properties are not evaluated, but the previous body is used. They both have only a view model as state, the same view model actually, but one have it as ObservedObject and the other as EnvironmentObject. I suspect it is a misuse of EnvironmentObject that I am doing, but I can't find any reference to why it is wrong
this works: (yes, i know, you know how to solve it, but i think this would be the "right" way.
problem is this line:
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel(). <<< problem
because the radioModel will be newly created each time the RadioListTest view is refreshed, so just create the instance one view above and it won't be created on every refresh (or do you want it to be created every time?!)
class RadioModel: ObservableObject {
#Published var selected: Int = 0
init() {
print("init radiomodel")
}
}
struct RadioButton<Content: View>: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#EnvironmentObject var radioModel: RadioModel
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton<Text>(idx: 0)
RadioButton<Text>(idx: 1)
RadioButton<Text>(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#ObservedObject var radioModel = RadioModel()
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest().environmentObject(radioModel)
}
}
}
}
What is wrong with this piece of code?
Your RadioListTest subview is not updated on refresh() because it does not depend on changed parameter (refreshDate in this case), so SwiftUI rendering engine assume it is equal to previously created and does nothing with it:
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest() // << here !!
}
so the solution is to make this view dependent somehow on changed parameter, if it is required of course, and here fixed variant
RadioListTest().id(refreshDate)
When I Googled "State vs ObservedObject" the first result was from Hacking with Swift and it said about #ObservedObject:
This is very similar to #State except now we’re using an external reference type rather than a simple local property like a string or an integer.
Can I use #ObservedObject to create persisted state? Is it as simple as #State is for simple properties and #ObservedObject is for complex objects, or is there more nuance to it?
#ObservedObject does not persist state
Can I use #ObservedObject to create persisted state?
On its own, you cannot. The Apple documentation has this to say about #State:
A persistent value of a given type, through which a view reads and monitors the value.
But I found no mention of persistence with #ObservedObject so I constructed this little demo which confirms that #ObservedObject does not persist state:
class Bar: ObservableObject {
#Published var value: Int
init(bar: Int) {
self.value = bar
}
}
struct ChildView: View {
let value: Int
#ObservedObject var bar: Bar = Bar(bar: 0)
var body: some View {
VStack(alignment: .trailing) {
Text("param value: \(value)")
Text("#ObservedObject bar: \(bar.value)")
Button("(child) bar.value++") {
self.bar.value += 1
}
}
}
}
struct ContentView: View {
#State var value = 0
var body: some View {
VStack {
Spacer()
Button("(parent) value++") {
self.value += 1
}
ChildView(value: value)
Spacer()
}
}
}
Whenever you click on the value++ button, it results in a re-render of ChildView because the value property changed. When a view is re-rendered as a result of a property change, it's #ObservedObjects are reset
In contrast, if you add a #State variable to the ChildView you'll notice that it's value is not reset when the #ObservedObject is reset.
Using persisted state with #ObservedObject
To persist state with #ObservedObject, instantiate the concrete ObservableObject with #State in the parent view. So to fix the previous example, would go like this:
struct ChildView: View {
let value: Int
#ObservedObject var bar: Bar // <-- passed in by parent view
var body: some View {
VStack(alignment: .trailing) {
Text("param value: \(value)")
Text("#ObservedObject bar: \(bar.value)")
Button("(child) bar.value++") {
self.bar.value += 1
}
}
}
}
struct ContentView: View {
#State var value = 0
#State var bar = Bar(bar: 0) // <-- The ObservableObject
var body: some View {
VStack {
Spacer()
Button("(parent) value++") {
self.value += 1
}
ChildView(value: value, bar: bar).id(1)
Spacer()
}
}
}
The definition of the class Bar is unchanged from the first code example. And now we see that the value is not reset even when the value property changes: