Better way use #State or #ObservableObject - swift

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.

Related

View does not update when changing RealmObject passed as binding

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.

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)
}
}

Do I have to use an ObservableObject in SwiftUI?

I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
A sample on what that looks like now :
import Foundation
import SwiftUI
struct Object3D {
var x : Int
var y : Int
var z : Int
var visible : Bool
}
struct NumberView<Number : Strideable> : View {
var label : String
#State var number : Number
var body : some View {
HStack {
TextField(
self.label,
value: self.$number,
formatter: NumberFormatter()
)
Stepper("", value: self.$number)
.labelsHidden()
}
}
}
struct ObjectInspector : View {
#State var object : Object3D
var body : some View {
VStack {
Form {
Toggle("Visible", isOn: $object.visible)
}
Divider()
Form {
HStack {
NumberView(label: "X:", number: object.x)
NumberView(label: "Y:", number: object.y)
NumberView(label: "Z:", number: object.z)
}
}
}
.padding()
}
}
struct ObjectInspector_Previews: PreviewProvider {
static var previews: some View {
ObjectInspector(object: Object3D(x: 0, y: 0, z: 0, visible: true))
}
}
You don't have to use #ObservedObject to ensure that updates to your model object are updating your view.
If you want to use a struct as your model object, you can use #State and your view will be updated correctly whenever your #State struct is updated.
There are lots of different property wrappers that you can use to update your SwiftUI views whenever your model object is updated. You can use both value and reference types as your model objects, however, depending on your choice, you have to use different property wrappers.
#State can only be used on value types and #State properties can only be updated from inside the view itself (hence they must be private).
#ObservedObject (and all other ...Object property wrappers, such as #EnvironmentObject and #StateObject) can only be used with classes that conform to ObservableObject. If you want to be able to update your model objects from both inside and outside your view, you must use an ObservableObject conformant type with the appropriate property wrapper, you cannot use #State in this case.
So think about what sources your model objects can be updated from (only from user input captured directly inside your View or from outside the view as well - such as from other views or from the network), whether you need value or reference semantics and make the appropriate choice for your data model accordingly.
For more information on the differences between #ObservedObject and #StateObject, see What is the difference between ObservedObject and StateObject in SwiftUI.
I would like to use a struct instead of a class as a state for my View, and as you may know, ObservableObject is a protocol only classes can conform to.
A model is usually shared among whichever parts of the app need it, so that they're all looking at the same data all the time. For that, you want a reference type (i.e. a class), so that everybody shares a single instance of the model. If you use a value type (i.e. a struct), your model will be copied each time you assign it to something. To make that work, you'd need to copy the updated info back to wherever it belongs whenever you finish updating it, and then arrange for every other part of the app that might use it to get an updated copy. It's usually a whole lot easier and safer to share one instance than to manage that sort of updating.
Do I have to wrap my struct in a ViewModel or some other similar type of object ? What happens if I don't ?
It's your code -- you can do whatever you like. ObservableObject provides a nice mechanism for communicating the fact that your model has changed to other parts of your program. It's not the only possible way to do that, but it's the way that SwiftUI does it, so if you go another route you're going to lose out on a lot of support that's built into SwiftUI.
The View is a struct already, it cannot be a class. It holds the data that SwiftUI diffs to update the actual UIViews and NSViews on screen. It uses the #State and #Binding property wrappers to make it behave like a class, i.e. so when the hierarchy of View structs is recreated they are given back their property values from last time. You can refactor groups of vars into their own testable struct and include mutating funcs.
You usually only need an ObservableObject if you are using Combine, it's part of the Combine framework.
I recommend watching Data Flow through SwiftUI WWDC 2019 for more detail.

How to persist data using MVVM in SwiftUI?

I’m practicing MVVM and SwiftUI by making a simple practice app. The main view of the app is a list where presents a title (in each cell) that can change by user input (text field). By selecting the cell, the app presents you the detail view where it presents another text.
I managed to change the cell´s title but I can’t figure out how to change the text in the detail view and make it stay that way. When I change the text in the detail view and go back to the main view, after entering again, the text doesn’t stay the same.
How can I make the text in the detail view maintain the text of whatever the user writes?
Your Sandwish is a struct which means when you pass it around it's copied (See structure vs class in swift language). This also means that when you pass a sandwish:
CPCell(sandwish: sandwish)
...
struct CPCell: View {
#State var sandwish: Sandwish
...
}
a sandwish is copied - any changes you make on this copy will not apply to the original sandwish.
When you do $sandwish.name in CPCell you're already binding to a copy. And in the NavigationLink you're copying it again.
struct CPCell: View {
...
var body: some View {
NavigationLink(destination: SandwishDetail(sandwish: sandwish)) {
TextField("Record", text: $sandwish.name)
}
}
}
So in the SandwishDetail you're already using a copy of a copy of your original sandwish.
struct SandwishDetail: View {
#State var sandwish: Sandwish // <- this is not the same struct as in `ContentView`
...
}
One way is to make Sandwish a class. Another, maybe better, solution is to use #Binding.
Note that the change from #State var sandwish to #Binding is not enough. CPCell expects the sandwish parameter to be Binding<Sandwish>, so you can't just pass a struct of type Sandwish.
One of the solutions is to use an index to access a binding from the SandwishStore:
ForEach (0..<store.sandwishes.count, id:\.self) { index in
CPCell(sandwish: self.$store.sandwishes[index])
}
...
struct CPCell: View {
#Binding var sandwish: Sandwish
...
}
Also you should do the same for all other places where the compiler expects Binding<Sandwish> and you originally passed Sandwish.
Change #State to #Binding in Sandwish.swift and in your CPCell struct in ContentView.swift

Pass a passed binding (#Binding)

If I pass a binding to another view can that 2nd view then pass the binding on and have the third view change the values in the first view or can this cause unexpected behavior?
For instance, if I have
struct FirstView: View {
#State var input: String = ""
var body: some View {
Form {
CustomTextField("Placeholder", $input)
}
}
}
struct CustomTextField: View {
#Binding var text: String
var body: some View {
ThirdView(text: $text)
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
TextField("Result", $text)
}
}
I know the above is nonsensical - I'm only using it for demonstration purposes - but would the ThirdView properly update the state of the first?
I have had instances where it works fine and others where it doesn't but can't really find much of an explanation.
If I pass a binding to another view can that 2nd view then pass the binding on and have the third view change the values in the first view or can this cause unexpected behavior?
By my observation there are following variants:
1) If those views live in one view hierarchy (or one in another) then such combination work
2) If those views are in different view hierarchies (or views are replaced, eg. by navigation) then binding works only on one level, but deeper transfer has unexpected behavior (most usual defect is that intermediate views are not updated).
Code in question works (tested with Xcode 11.4 / iOS 13.4), because fits variant 1.