View does not update when changing RealmObject passed as binding - swift

I have a view, where I have an array of RealmObjects. When I pass them into another View via #Binding and try to edit them there, the View does not get updated.
This is a simplified example. When the name changes, it dumps the correct name into the console, but the View does not update. Tried to force a reload by changing the .id() of the View, but this doesn't help either.
class Person: Object, ObjectIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = ""
}
struct ParentView: View {
#State private var persons: [Person] = []
var body: some View {
ChildView(persons: $persons)
}
}
struct ChildView: View {
#Binding var persons: [Person]
var body: some View {
ForEach(person) { person in
Text(person.name)
.onTapGesture {
person.name = "New Name"
dump(person)
}
}
}
}

One issue is mixing two different technologies by assigning Realm objects to a Swift array - generally speaking, that may not be the best idea.
Realm objects are lazily-loaded and massive datasets take almost no memory. However, as soon as those objects are cast to Swift functions and constructs they are all loaded into memory and can overwhelm the device.
Best practice is to keep Realm objects within Realm stuctures e.g. Collections like Results and Lists.
To address the question:
Realm provides an object #ObservedResults which is a property wrapper that invalides a view when observed objects change. e.g. If the app has #ObservedResults of a group of People objects, if one of those changes, the associated view will also update. From the docs
You can use this property wrapper to create a view that automatically
updates itself when the observed object changes.
So change this
struct ChildView: View {
#Binding var persons: [Person]
to this
struct ChildView: View {
#ObservedResults(Person.self) var persons
I think the rest of the code is good to go as is.

Related

A struct mutating properties or ObservableObject Published properties to drive data changes

I would like help to further understand the implications of using the following 2 methods for driving data between multiple views.
My situation:
A parent view initialises multiple child views with data passed in.
This data is a big object.
Each view takes a different slice of the data.
Each view can manipulate the initial data (filtering, ordering etc)
Using an observableObeject to store this data and multiple published properties for each view :
can be passed in as an environment object that can be accessed by any view using #EnvironmentObject.
You can create a Binding to the published properties and change them.
Execute a method on the ObservableObject class and manipulate a property value which gets published using objectWillChange.send() inside the method.
I have achieved the desired listed above by using a struct with mutating methods. Once these properties are changed in the struct, the views which bind to these properties causes a re-render.
My struct does not do any async work. It sets initial values. Its properties are modified upon user action like clicking filter buttons.
Example
struct MyStruct {
var prop1 = "hello"
var prop2: [String] = []
init(prop2: [String]) {
self.prop2 = prop2
}
mutating func changeProp2(multiplier: Int) {
let computation = ...
prop2 = computation //<----- This mutates prop2 and so my view Binded to this value gets re-renderd.
}
}
struct ParentView: View {
var initValue: [String] // <- passed in from ContentView
#State private var myStruct: MyStruct
init(initValue: [String]) {
self.myStruct = MyStruct(prop2: initValue)
}
var body: some View {
VStack {
SiblingOne(myStruct: $myStruct)
SiblingTwo(myStruct: $myStruct)
}
}
}
struct SiblingOne: View {
#Binding var myStruct: MyStruct
var body: some View {
HStack{
Button {
myStruct.changeProp2(multiplier: 10)
} label: {
Text("Mutate Prop 2")
}
}
}
}
struct SiblingTwo: View {
#Binding var myStruct: MyStruct
var body: some View {
ForEach(Array(myStruct.prop2.enumerated()), id: \.offset) { idx, val in
Text(val)
}
}
}
Question:
What use cases are there for using an ObservableObject than using a struct that mutates its own properties?
There are overlap use cases however I wish to understand the differences where:
Some situation A favours ObservableObject
Some situation B favours struct mutating properties
Before I begin, when you say "these properties causes a re-render" nothing is actually re-rendered all that happens is all the body that depend on lets and #State vars that have changed are invoked and SwiftUI builds a tree of these values. This is super fast because its just creating values on the memory stack. It diffs this value tree with the previous and the differences are used to create/update/remove UIView objects on screen. The actual rendering is another level below that. So we refer to this as invalidation rather than render. It's good practice to "tighten" the invalidation for better performance, i.e. only declare lets/vars in that View that are actually used in the body to make it shorter. That being said no one has ever compared the performance between one large body and many small ones so the real gains are an unknown at the moment. Since these trees of values are created and thrown away it is important to only init value types and not any objects, e.g. don't init any NSNumberFormatter or NSPredicate objects as a View struct's let because they are instantly lost which is essentially a memory leak! Objects need to be in property wrappers so they are only init once.
In both of your example situations its best to prefer value types, i.e. structs to hold the data. If there is just simple mutating logic then use #State var struct with mutating funcs and pass it into subviews as a let if you need read access or #Binding var struct if you need write access.
If you need to persist or sync the data then that is when you would benefit from a reference type, i.e. an ObservableObject. Since objects are created on the memory heap these are more expensive to create so we should limit their use. If you would like the object's life cycle to be tied to something on screen then use #StateObject. We typically used one of these to download data but that is no longer needed now that we have .task which has the added benefit it will cancel the download automatically when the view dissapears, which no one remembered to do with #StateObject. However, if it is the model data that will never be deinit, e.g. the model structs will be loaded from disk and saved (asynchronously), then it's best to use a singleton object, and pass it in to the View hierarchy as an environment object, e.g. .environmentObject(Store.shared), then for previews you can use a model that is init with sample data rather that loaded from disk, e.g. .environmentObject(Store.preview). The advantage here is that the object can be passed into Views deep in the hierarchy without passing them all down as let object (not #ObservedObject because we wouldn't want body invovked on these intermediary Views that don't use the object).
The other important thing is your item struct should usually conform to Identifiable so you can use it in a ForEach View. I noticed in your code you used ForEach like a for loop on array indices, that's a mistake and will cause a crash. It's a View that you need to supply with Indentifiable data so it can track changes, i.e. moves, insertions, deletions. That is simply not possible with array indices, because if the item moves from 0 to 1 it still appears as 0.
Here are some examples of all that:
struct UserItem: Identifiable {
var username: String
var id: String {
username
}
}
class Store: ObservableObject {
static var shared = Store()
static var preview = Store(preview: true)
#Published var users: [UserItem] = []
init(preview: Bool = false) {
if (preview) {
users = loadSampleUsers()
}
else {
users = loadUsersFromDisk()
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Store.shared)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach($store.users) { $user in
UserView(user: $user)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Store.preview)
}
}
struct UserView: View {
#Binding var user: UserItem
var body: some View {
TextField("Username", text: $user.username)
}
}

Better way use #State or #ObservableObject

Hi everyone I have a question about #State vs #ObservableObject with SwiftUI
I have a view that contains a LazyHGrid
To have a custom cell of the LazyHGrid I preferred to create a new struct with the custom cell.
The view hierarchy is composed as follows:
struct View1 -> struct LazyHGrid -> struct LazyHGridCustomCell
In View1 I have a text that must be replaced with content of the LazyHGridCustomCell every time it is selected.
At this point in view of my hierarchy should I use #State & #Binding to update the text or would it be better #ObservableObject?
In case I wanted to use the #State wrapper I would find myself like this:
struct View1 (#State)
struct LazyHGrid (#Binding)
struct LazyHGridCustomCell (#Binding)
I was wondering if this is the right way or consider #ObservableObject
I created a code example based on my question .. It was created just to let you understand what I mean to avoid being misunderstood
I was wondering if it is right to create such a situation or use an #ObservableObject
In case this path is wrong can you show me an example of the right way to go to get the correct result?
Thanks for suggestion
struct View1: View {
#State private var name: String
var body: some View {
Text(name)
LazyHGridView(name: $name)
}
}
struct LazyHGridView: View {
#Binding var name: String
var body: some View {
LazyHGrid(rows: Array(repeating: GridItem(), count: 2)) {
ForEach(reservationTimeItems) { item in
LazyHGridCustomCell(name: $name)
}
}
}
}
struct LazyHGridCustomCell: View {
#Binding var name: String
var body: some View {
Text(name)
.foregroundColor(.white)
}
}
According to Data Essentials in SwiftUI (WWDC 2020) at 9:46, you should be using State because ObservableObject is for model data.
State is designed for transient UI state that is local to a view. In
this section, I want to move your attention to designing your model
and explain all the tools that SwiftUI provides to you. Typically, in
your app, you store and process data by using a data model that is
separate from its UI. This is when you reach a critical point where
you need to manage the life cycle of your data, including persisting
and syncing it, handle side-effects, and, more generally, integrate it
with existing components. This is when you should use
ObservableObject. First, let's take a look at how ObservableObject is
defined.

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI bind array of objects and show changes

I'm trying to build a little app in SwiftUI. It is supposed to show a list of items an maybe change those. However, I am not able to figure out, how the data flow works correctly, so that changes will be reflected in my list.
Let's say I have a class of Item like this:
class Item: Identifiable {
let id = UUID()
var name: String
var dateCreated: Date
}
And this class has an initializer, that assigns each member a useful random value.
Now let's say I want to store a list of items in another class like this:
class ItemStore {
var items = [Item]()
}
This item store is part of my SceneDelegate and is handed to the ContextView.
Now what I want to do is hand one element to another view (from the stack of a NavigationView), where it will be changed, but I don't know how to save the changes made so that they will be reflected in the list, that is shown in the ContextView.
My idea is to make the item store an environment object. But what do I have to do within the item class and how do I have to pass the item to the other view, so that this works?
I already tried something with the videos from Apple's WWDC, but the wrappers there are deprecated, so that didn't work.
Any help is appreciated. Thanks a lot!
The possible approach is to use ObservableObject (from Combine) for storage
class ItemStore: ObservableObject {
#Published var items = [Item]()
// ... other code
}
class Item: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
#Published var dateCreated: Date
// ... other code
}
and in dependent views
struct ItemStoreView: View {
#ObservedObject var store: ItemStore
// ... other code
}
struct ItemView: View {
#ObservedObject var item: Item
// ... other code
}

LSP and SwiftUI

In order to make my code testable, I'm trying to adhere to Liskov's substitution principle by having my SwiftUI views depend on protocols rather than concrete types. This allows me to easily swap implementations and allows me to easily build mocks for testing. Here's an example of what I'm trying to do:
protocol DashboardViewModel: ObservableObject {
var orders: [Order] { get }
}
My DashboardViewModel needs to communicate changes back to its dependents, so I've also attached ObservableObject as a transitive requirement.
This seems to be a problem. You cannot achieve LSP if you have associated type requirements. Here's the error I got from my SwiftUI view class that depended on my view model:
struct DashboardView: View {
#ObservedObject var viewModel: DashboardViewModel
}
Protocol 'DatastoreProtocol' can only be used as a generic constraint because it has Self or associated type requirements
I ended up doing this instead:
protocol DashboardViewModel {
var orders: [Order] { get }
var objectWillChange: AnyPublisher<Void, Never> { get }
}
This also requires dependents to do additional work to observe for state changes. This takes away the conveniences of using property wrappers - mainly the ability for dependents to observe for state changes using #ObservedObject. Using this alternative leads us to code like this:
struct DashboardView: View {
let viewModel: DashboardViewModel
var viewModelSubscriber: AnyCancellable!
// MARK: - Used only to force a re-render of this view
#State private var reload = false
init(viewModel: DashboardViewModel) {
self.viewModel = viewModel
viewModelSubscriber = viewModel.objectWillChange.sink { _ in
self.reload.toggle()
}
}
}
This is a tad distasteful to look at:
Created a #State variable that is only used to force an update of the view, because we cannot utilize SwiftUI property wrappers to observe for state changes
Created a AnyCancellable! variable to hold a subscription to objectWillChange from the view model. This is necessary to detect state changes from the DashboardViewModel
Added the subscription call in the initializer that only toggles the #State variable to force a retrieval of new data from the view model
I feel like there should be a much better way of handling this. Looking for help!
One way to solve this is to make your view generic:
protocol DashboardViewModel: ObservableObject {
var orders: [Order] { get }
}
struct DashboardView<Model: DashboardViewModel>: View {
#ObservedObject var viewModel: Model
}