What is the proper way to share state among views with Swift Data?

So I'm building a relatively simple app using all the latest iOS 17 SwiftUI and Swift Data. I'm trying to follow Apple sample code and the "Model View" pattern (which I find pretty helpful Building Large Scale Apps Swiftui | AzamSharp )

It's not clear to me what is the right way to share data between child views, as they don't update as I would expect. I would think that using an object with @Model which of course also gives it @Observable that putting more logic in those to update Views would be the elegant solution, but the Views don't always update as I would expect. (like if you had a @State and a @Binding)

So below is my example code, and here are some questions I have that I haven't found clear answers to. (I apologize this is quite long, but I wasn't sure how else to ask, I'm looking for deeper understanding not just a "make it work" but rather learning best practices.)

  1. when I pass my @Model object, in this case Jacket from one view to another, is it creating a copy or is it a shared reference? I thought since its a class its shared, so if I make a change on one it affects the other, however my views don't seem to behave that way. e.g. what is my source of truth?

  2. is this the right way to share logic between views, just using the @Model object for it all? I thought since it had @Observable this would be the most elegant solution. However, maybe I should create a @State or @StateObject separately (a viewModel, coordinator etc.) and take that logic out of my Jacket @Model?

  3. If I mark Jacket @Bindable to toggle state (like a modal), it does not happen right away, only when the view is dismissed and reloaded, is this a problem with needing the main thread and @MainActor or something else?

  4. In my content view, the detail view Jacket is coming from a let from the selection ID on the array of Jackets. (this is how some Apple code demonstrated it with SwiftData Queries) However, am I losing the ability for them to share state this way? Apple elsewhere recommended selection being the type of the object itself. And in that case, should it then be a @Bindable var even if I don't have bindings but want children to update parents and vice versa?

import SwiftUI
import SwiftData

struct Sample: View {
    @Query private var jackets: [Jacket]
    
    @Environment(NavigationModel.self) private var navModel

    var body: some View {
        NavigationSplitView {
            @Bindable var navModel = navModel
            
            List(jackets, selection: $navModel.selectedId) { jacket in
                JacketRow(jacket: jacket)
            }
            
        } detail: {
            if let jacket = jackets[navModel.selectedId] {
                JacketDetail(jacket: jacket)
            } else {
                ContentUnavailableView("Nothing selected", systemImage: "filemenu.and.selection")
            }
        }
    }
}

struct JacketDetail: View {
    var jacket: Jacket
    
    var body: some View {
        VStack {
            Text(jacket.title)
                .font(.title)
            JacketChildDetail(jacket: jacket)
                .padding()
        }
    }
}

struct JacketChildDetail: View {
    var jacket: Jacket
    
    var body: some View {
        if jacket.frontImageData {
            // some view
        } else {
            // some other view
        }
    }
}
1 Like

I'm currently having the same issue. Is there any update on this?

I'm still unsure about an ideal solution, so would be happy for others to chime in.

Ultimately what I did was once you get the individual object (Jacket in my case) from the list and use it in your detail view (for example in JacketDetail) then, for any child views which you may want to compose that detail view with, you must break off the individual properties as their own Binding vars or similar in the child view.

(e.g. JacketChildDetail would NOT have another Jacket var, but instead whatever properties from Jacket that you need as Bindings such as frontImageData or whatever.)

As such you can compose your child views to only need certain bindings from the main swift data object so it all feels mostly composable and everything updates as it should.

I'm also seeing the same thing in my app - I have child views from a root view that contains my @Model object and passes them down to the children.

I'm convinced this is a bug from my playing around..

Here's what I've found in a nutshell before making the @Observable wrapper...

  • Insert models -> delete all the model objects -> reinsert new instances with the same data
    • The views update as expected
  • Insert models -> delete all the model objects -> reinsert the same instance
    • Nothing is inserted
    • Not sure if this is intended but assuming it's correct as the persistent IDs will be the same as the ones just deleted? (Initially surprised me as I'd expect it to be inserted but I'm not sure on this one...)
  • Insert models -> update those same model instance properties
    • The cells redraw
  • Insert models -> "upsert" (insert new instance but matches same attributed "unique" property)
    • The views are stale (scrolling my List so the rows move in and out of view redraws them, sounds like the classic old cell redrawing as I'm sure this will be a collection view or something under the hood)

I should also point out just in case there's any question:

  • I'm using an in memory store in my app as it's just a lightweight menu bar app, but this shouldn't make a difference
  • The app works on Mac and iOS and I'm seeing the same thing

When implementing the fix/workaround/whatever we want to call it using the @Observable, I ended up creating a generic much like Optional to help me reuse my code if anyone else finds it handy.

import Foundation
import SwiftData

@Observable
class ObservedModel<T: PersistentModel> {
    let wrapped: T
    
    init(wrapped: T) {
        self.wrapped = wrapped
    }
}