SwiftUI MacOS using ObservableObject to update State - swift

I am having a State in SwiftUI Mac OS, which stores my active selection of a Navigation View. Everything is working with the Navigation view.
Now I have created a new class which confirms to the Observable Object. In some child views I will make a change to that object. When the change is done, my Navigation view updated the object aswell, which is the feature of Observable Object as far as I understand.
What I now want to achieve is that the Observable object changes my #State in my Navigation view.
That is my declaration in my Navigation view. UserData stores a int aswell, which should be set to the selection on change.
#EnvironmentObject var userData: UserData
#State var selection: Int?
So userData.active = 2, should set selection = 2 aswell. Is there a onChange event I can trigger?
I am using that #State selection for a Binding in my Navigation Link.
NavigationLink(destination: SecondContentView(), tag: 0, selection: self.$selection)
{
Second approach, would be using that userData.active : Int directly as State. However, I am passing that selection State as Binding and it gives me an error when passing the variable of an EnvironmentObject as Binding.

Try to use UserData directly, like
NavigationLink(destination: SecondContentView(), tag: 0,
selection: self.$userData.active)

Related

SwiftUI Update NavigationView > List > ForEach with .refreshable?

I have code with NavigationView and List view that contains a ForEach with NavigationLink. My problem is in the view when user pressing button save with user info id, username etc. in ContentView that info appears only after refreshing app, so my question what logic I can put in .refreshable modifier?
#ObservedObject var habit = Habit()
...
ForEach(0..<habit.items.count, id: .self) { index in
NavigationLink(destination: HabitView(habit: self.habit, index: index)) {
...
I think that this link demonstrates one possible solution, for another way, but it exceeds my skills in programming: https://troz.net/post/2019/swiftui-data-flow/ .
I think in this situation is a simpler solution than to change all of the code.

SwiftUI ForEach.onDelete properly animates but then resets

Trying to figure out why my ForEach loop isn’t updating. I have the model marked as ObservedObject and have done everything I could to make sure the updates were happening. I even saw that the model was being updated while printing.
class Model {
var array: [Int]
}
…
struct ModelView: View {
#ObservedObject var model: Model
var body: some View {
List {
ForEach(model.array.indices, id:\.self) { index in
…
}.onDelete(perform: delete)
}
}
The row is animating and acting like it is deleting and does delete in the model, however the deleted row animates back in and the original data set is shown!
I figured it out. I needed to make the array in my model object with the #Published property wrapper. Doing that fixed everything!

Binding #Published property of parent view's viewModel to #Published property of child view's viewModel: SwiftUI

I have this setup:
Parent View: PostsListView
Child View: PostEditView
PostsListView shows a list short description of posts
On tap of any post, I present child view: PostEditView
Goal is to chage post here call API and when user comes back to PostsListView, it should show updated description
I'm using MVVM so, in PostsListView
#ObservedObject var viewModel : PostsListViewModel
LazyVGrid(columns: columns) {
ForEach(viewModel.posts, id: \.self) { post in
NavigationLink(
destination: PostEditView(viewModel: PostEditViewModel(post: post))){
Text(post: post)
.lineLimit(3)
}
}
}
I pass view model(PostEditViewModel(post:)) to childView from parent view.
I'm not sure how to bind this 'post' object across both views' viewModels.
FYI viewModels:
PostsListViewModel:
class PostsListViewModel: ObservableObject{
#Published var posts: [Post]
//api operations....
}
PostEditViewModel:
class PostEditViewModel: ObservableObject{
#Published var post: Post
//custom Post mutation and api operations....
}
You don't need to do any special Combine operations. Just make sure that Post is a reference type (class) and pass the Post instance from PostListViewModel.posts to PostEditViewModel.post, this way whenever you mutate any properties of the Post object in the child VM, the same changes will be reflected on the parent VM as well, since the Post object is a reference type and both VMs have a reference to the same object.

How can I prevent SwiftUI from reinitializing my wrapped property just like it does with #StateObject?

From what I've read whenever you instantiate an object yourself in your view, you should use #StateObject instead of #ObservedObject. Because apparently if you use #ObservedObject, SwiftUI might decide in any moment to throw it away and recreate it again later, which could be expensive for some objects. But if you use #StateObject instead then apparently SwiftUI knows not to throw it away.
Am I understanding that correctly?
My question is, how does #StateObject communicate that to SwiftUI? The reason I'm asking is because I've made my own propertyWrapper which connects a view's property to a firebase firestore collection and then starts listening for live snapshots. Here's an example of what that looks like:
struct Room: Model {
#DocumentID
var id: DocumentReference? = nil
var title: String
var description: String
static let collectionPath: String = "rooms"
}
struct MacOSView: View {
#Collection({ $0.order(by: "title") })
private var rooms: [Room]
var body: some View {
NavigationView {
List(rooms) { room in
NavigationLink(
destination: Lazy(ChatRoom(room))
) {
Text(room.title)
}
}
}
}
}
The closure inside #Collection is optional, but can be used to build a more precise query for the collection.
Now this works very nicely. The code is expressive and I get nice live-updating data. You can see that when the user would click on a room title, the navigation view would navigate to that chat room. The chatroom is a view which shows all the messages in that room. Here's a simplified view of that code:
struct ChatRoom: View {
#Collection(wait: true)
private var messages: [Message]
// I'm using (wait: true) to say "don't open a connection just yet,
// because I need to give you more information that I can't give you yet"
// And that's because I need to give #Collection a query
// based on the room id, which I can only do in the initializer.
init(_ room: Room) {
_messages = Collection { query in
query
.whereField("room", isEqualTo: room.id!)
.order(by: "created")
}
}
var body: some View {
List(messages) { message in
MessageBubble(message: message)
}
}
}
But what I've noticed, is that SwiftUI initializes a new messages-collection every single time the user interacts with the UI in any way. Like even when an input box is clicked to give it focus. This is a huge memory leak. I don't understand why this happens, but is there a way to tell SwiftUI to only initialize #Collection once, just like it does with #StateObject?
Use #StateObject when you want to define a new source-of-truth reference type owned by that View and tied to its life-cycle. The object will be created just before the body is run for the first time and is stored in a special place by SwiftUI. If the View struct is recreated (e.g. by a parent View's body recompute by a state change) then the previous object will be set on the property instead of a new object. If the View is no longer init during body updates then the object will be deinit.
When you pass a #StateObject into a child View then in that child View uses #ObservedObject to enable the body of the child view to be recomputed when the object changes, just the same way the body of the parent View will also be recomputed because it used an #StateObject. If you use #ObservedObject for an object that was not an #StateObject in a parent View then since no View is owning it then it will be lost every time the View is init during body updates. Also, any objects that you create in the View init, like your Collection, those are immediately lost too. These excessive heap allocations could cause a leak and slows down SwiftUI. Objects the View owns must be wrapped in #StateObject to avoid this.
Lastly, don't use #StateObject for view state and certainly don't try and implement a legacy "View Model" pattern with them. We have #State and #Binding for that. #StateObject is only for model objects (as in your domain types: Book, Person etc.), loaders or fetchers.
WWDC 2020 Data Essentials in SwiftUI explains all this very nicely.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.