I'm building an app that has a pretty involved data model - a number of hierarchies of data - and I have it modelled as a value type that starts at the top and just includes everything so that my entire data model is a Document struct.
An simple example:
struct Step {
var name = "First Step"
}
struct Scene {
var step = Step()
}
struct Document {
var scene = Scene()
}
var doc = Document()
If I find a TextField to $doc.scene.step.name then every character the user types is going to update the entire document. By that, I mean a didSet on the scene property of a Document fires, etc., all the way down.
Is there a better way to do this?
In UIKit, I'd create an initialize a local variable that the user edits inside the view controller, and then update the model when the user leaves the view or the editor loses focus. I've tried to do something similar in SwiftUI by setting a local value in onAppear and updating it when the view disappears but this is awkward to do for every field and there are edge cases that make it difficult.
Just curious if there's a pattern for this that I'm missing.
Yes, I'm experiencing laggy typing and dropped characters when typing into the text field that's in the detail pane of a NavigationSplitView, only on iPad. I created a DTS case and it was closed as "under investigation, no workaround" so I'm just trying to figure out what to do.
If I present the view that's in the detail view in a sheet modally and let the user hit a Save button to dismiss it, that works fine. It's when the TextField is bound to the app-wide state that the issue occurs.
It might be a bug I'm running into, because you're right, what I'm doing seems like it should work. I've tried to produce a minimal sample but a simple app that does the same thing doesn't show the bug.
With Observation seeming to prefer reference over value types, I'm wondering if that's not a better path anyway.
The thing about using a tree of structs like this is that changing any value in any part of it causes the whole thing to signal that it changed and all views observing it are redrawn. I didn’t know about didSet being called, not clear why, but changing ‘name’ is causing your views from doc on down to redraw and I suspect why performance is sluggish.
This is something I never understood because many examples will be set up in this way where far more is redrawn than necessary, such as this…
struct User: Identifiable { … }
struct ContentView: View {
@State var users = [User("Bob"), User("Sally"), User("Niloufar")]
var body: some View {
ForEach($users) { $user in
EditUserView(user: $user)
}
}
}
Here, changing any of the individual users in EditUserView causes the whole list of users to be redrawn even though only 1 changed. I guess computers are fast enough they don’t mind the inefficiency.
In practice though what I think people who model with structs do is have a database. The ‘tree’ is built out of ids and associations in the DB so the structs are just small clumps of individual data not directly connected to the larger structure and in this way it doesn’t redrawn everything.
I don’t have easy access to a DB though and make my models as a tree of ObservableObjects. In this way only the node being changed emits that it changed and only views observing that node get redrawn. I find this more flexible in controlling how my views respond, however there’s caveats, especially if you’re deep-reaching into the tree like $doc.scene.step.name. If some view is showing that name as…
Text(doc.scene.step.name)
…then it’s not necessarily updated unless the view is observing that step instance. Because of this my views usually only work directly with one node/object or crafted to observe exactly what they need or sometimes a little Combine saves the day. I haven’t tried the new Observable framework to know if deep-reaching works out of the box there.
Anyways, maybe all this redrawing is not the source of your issue. You could try disconnecting the name string from the struct tree somehow, like turn Step into an ObservableObject or just have a State property in the relevant view and see if the problem persists. To know when views are redrawing I use a function that returns a random color and sprinkle that around in view backgrounds.