Does SwiftUI removeDuplicates from a subscriber to an #Published property? - swiftui

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

Related

When does a View in a List get removed from the stack after it disappears from view?

Given the following code:
import SwiftUI
struct ContentView: View {
var elements = Array(1...100)
var body: some View {
List(elements, id: \.self) { element in
ElementView()
}
}
}
struct ElementView: View {
let viewModel = ViewModel()
var body: some View {
Text("Row")
}
}
class ViewModel {
deinit {
print("Deinit")
}
}
When the list is scrolled to the end, Deinit is never printed indicating that all ViewModels are in memory. If all ViewModels are in memory then all ElementViews must still be on the stack as each one holds a reference to a ViewModel.
Can someone provide documentation on the lifecycle of a View inside List?

SwiftUI Navigation popping back when modifying list binding property in a pushed view

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.

Is there a way decouple views from view models like the following?

My target is 2 thing:
1. to make a view depending on a view model protocol not a concrete class.
2. a sub view gets the view model from the environment instead of passing it through the view hierarchy
I've mentioned my goals so if there's a totally different way to achieve them, I'm open to suggestion.
Here's what've tried and failed of course and raised weird error:
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
struct MyView: View {
#EnvironmentObject var viewModel: some ViewModelProtocol
var body: some View {
HStack {
TextField("Enter something...", text:$viewModel.text)
Text(viewModel.greetings)
}
}
}
//MARK:- View Model
protocol ViewModelProtocol: ObservableObject {
var greetings: String { get }
var text: String { get set }
}
class ConcreteViewModel: ViewModelProtocol {
var greetings: String { "Hello everyone..!" }
#Published var text = ""
}
//MARK:- Usage
let parent = ContentView().environmentObject(ConcreteViewModel())
Yes there is, but it's not very pretty.
You're running into issues, since the compiler can't understand how it's ever supposed to infer what type that that some protocol should be.
The reason why some works in declaring your view, is that it's inferred from the type of whatever you supply to it.
If you make your view struct take a generic viewmodel type, then you can get this up and compiling.
struct MyView<ViewModel: ViewModelProtocol>: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Text(viewModel.greetings)
}
}
the bummer here, is that you now have to declare the type of viewmodel whenever you use this view, like so:
let test: MyView<ConcreteViewModel> = MyView()

Classes and Observable Object

I'm trying to make a data model class that can be referenced by different views. The data model has a function that can modify one of its published variables. However, this function is called inside one view, the change it makes to the published variable is not reflected in other views which also reference the class. The most simple example I can come up with is this:
struct ContentView: View {
var body: some View {
VStack {
TextView()
ButtonView()
}
}
}
struct TextView: View {
#ObservedObject var data = Data()
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data = Data()
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
class Data: ObservableObject {
#Published var currentWord = "Cat"
func randomWord() {
let word = ["Cat", "Dog", "Mouse", "Horse"].randomElement()!
print(word)
currentWord = word
}
}
Both the ButtonView and TextView reference the same class, and the ButtonView calls the 'Data' class's method 'randomWord' which modifies its 'currentWord' published variable. However, the change to this variable is not reflected in the Text of the TextView which also references the 'Data' class.
I think I'm not understanding something about classes and observableObject correctly. Would anyone be kind enough to point out my mistake here?
You create two different instance of Data in your subviews, instead you need to share one, so create it in ContentView and pass to subviews as below
struct ContentView: View {
#ObservedObject var data = Data()
var body: some View {
VStack {
TextView(data: data)
ButtonView(data: data)
}
}
}
struct TextView: View {
#ObservedObject var data: Data
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data: Data
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
Also, as variant, for such scenario can be used EnvironmentObject pattern. There are a lot of examples here on SO you can find about environment objects usage - just search by keywords.

What is the difference between #State and #ObservedObject, can they both be used to persist state?

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: