I would suggest to follow this logic. When you pass your var layer: Layer1
to subviews, you can save and delete to db without issue. The issue arises when you need to update the view. @Query
creates kind of "pipeline" and your items are updated once they are in context. So to make your views get those updates using @Query you can do as in below code. Maybe there are other options out there. But this one I spent some time doing myself. I have created more readable code, sorry those layer1
and layer2
got my brain spinning. I also added comments so I think it clear what is going on in the code. I tend to be perfectionist way too often so hope you don't mind that ))) But the data flow is the same as in your code.
UPDATE
My previous suggestion works, however there is redundant code in it, due to using @Query
and custom inits for that part. What is actually needed for the model to work flawlessly is revision of relationships. One part MUST be optional, (it is commented in the code) and how we add child to children relationship and grandchild to grandchildren respectively. Also preview is updated to show proper information. So code is updated to show the latest info. The previous part using @Query
I deleted as many lines would add to the post :)
NOW ABOUT THE REASON why your code did not work
Paul mentioned that this might be a bug in that article. https://www.hackingwithswift.com/quick-start/swiftdata/how-to-save-a-swiftdata-object
Namely:
If you use a non-optional on the other side (your case), you must specify the delete rule manually (you did it) and call save() (your code does not include that) when inserting the data, otherwise SwiftData won’t refresh the relationship (as in your case) until application is relaunched – even if you call save() at a later date, and even if you create and run a new FetchDescriptor from scratch.
1. The App File
import SwiftUI
@main
struct SwiftDataPassingToChildApp: App {
var body: some Scene {
WindowGroup {
ParentView()
.modelContainer(for: Parent.self)
}
}
}
2. Data Model File
import Foundation
import SwiftData
@Model
final class Parent: Hashable {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Child.parent)
var children: [Child] = []
init(name: String) {
self.name = name
}
}
@Model
final class Child: Hashable {
var name: String
// The 1 side needs to be optional or you get an
// "Unsupported relationship" fatal error when running the app
var parent: Parent?
@Relationship(deleteRule: .cascade, inverse: \Grandchild.child)
var grandChildren: [Grandchild] = []
init(name: String, parent: Parent) {
self.name = name
self.parent = parent
}
}
@Model
final class Grandchild: Hashable {
var name: String
// The 1 side needs to be optional or you get an
// "Unsupported relationship" fatal error when running the app
var child: Child?
init(name: String, child: Child) {
self.name = name
self.child = child
}
}
extension Parent {
// Mock Data
@MainActor
static var preview: ModelContainer {
let container = try! ModelContainer(for: Parent.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
let parent1 = Parent(name: "Parent One")
let parent2 = Parent(name: "Parent Two")
let child1 = Child(name: "Child One", parent: parent1)
let child2 = Child(name: "Child Two", parent: parent2)
let grandChild1 = Grandchild(name: "Grandchild One", child: child1)
let grandChild2 = Grandchild(name: "Grancdhild Two", child: child2)
container.mainContext.insert(grandChild1)
container.mainContext.insert(grandChild2)
return container
}
}
3. Parent View File
import SwiftUI
import SwiftData
struct ParentView: View {
// We create "pipeline" so whenever data changes in the model context
// the view automatically gets updated, this is because all models are observable
@Query private var parents: [Parent]
@Environment(\.modelContext) var modelContext
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
if parents.isEmpty {
emptyView
} else {
parentViewSection
}
}
}
}
// MARK: - Extensions
extension ParentView {
// MARK: - View Sections
private var emptyView: some View {
ContentUnavailableView {
Label("No Parent Items", systemImage: "person.fill")
} description: {
Text("You don't have any parent items yet.\n Add new items now!")
} actions: {
Button {
addParent()
} label: {
Text("Add Parent Item")
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
}
}
private var parentViewSection: some View {
List {
ForEach(parents) { parent in
NavigationLink(value: parent) {
Text(parent.name)
}
}
.onDelete(perform: deleteParent)
}
.navigationDestination(for: Parent.self) { parent in
ChildView(selectedParent: parent)
}
.toolbar {
Button("Add Parent", systemImage: "person.fill") {
addParent()
}
}
.navigationTitle("Parent View")
}
// MARK: - Functions
private func addParent() {
let parent = Parent(name: UUID().uuidString)
modelContext.insert(parent)
}
private func deleteParent(indexSet: IndexSet) {
for index in indexSet {
modelContext.delete(parents[index])
}
}
}
// MARK: - Preview
#Preview {
NavigationStack {
ParentView()
.modelContainer(Parent.preview)
}
}
4. Child View File
import SwiftUI
import SwiftData
struct ChildView: View {
@Environment(\.modelContext) var modelContext
let selectedParent: Parent
var body: some View {
if selectedParent.children.isEmpty {
emptyView
} else {
childrenViewSection
}
}
}
// MARK: - Extensions
extension ChildView {
// MARK: - View Sections
private var emptyView: some View {
ContentUnavailableView {
Label("No Children Items", systemImage: "person.2.fill")
} description: {
Text("You don't have any children items yet.\n Add new items now!")
} actions: {
Button {
addChild()
} label: {
Text("Add Child Item")
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
}
}
private var childrenViewSection: some View {
List {
ForEach(selectedParent.children) { child in
NavigationLink(value: child) {
Text(child.name)
}
}
.onDelete(perform: deleteChild)
}
.navigationDestination(for: Child.self) { child in
GrandchildView(selectedChild: child)
}
.toolbar {
Button("Add Child", systemImage: "person.2.fill") {
addChild()
}
}
.navigationTitle("Child View")
}
// MARK: - Functions
private func addChild() {
let child = Child(name: UUID().uuidString, parent: selectedParent)
// We are adding child to parent like this
selectedParent.children.append(child)
}
private func deleteChild(indexSet: IndexSet) {
for index in indexSet {
modelContext.delete(selectedParent.children[index])
}
}
}
// MARK: - Preview
#Preview {
let context = Parent.preview.mainContext
let parent = Parent(name: "Parent One")
let child = Child(name: "Child One", parent: parent)
let grandchild = Grandchild(name: "Grandchild One", child: child)
context.insert(grandchild)
return NavigationStack {
ChildView(selectedParent: parent)
}
}
5. Grandchild View File
import SwiftUI
import SwiftData
struct GrandchildView: View {
@Environment(\.modelContext) var modelContext
let selectedChild: Child
var body: some View {
if selectedChild.grandChildren.isEmpty {
emptyView
} else {
grandchildrenViewSection
}
}
}
// MARK: - Extensions
extension GrandchildView {
// MARK: - View Sections
private var emptyView: some View {
ContentUnavailableView {
Label("No Grandchildren Items", systemImage: "person.3.fill")
} description: {
Text("You don't have any grandchildren items yet.\n Add new items now!")
} actions: {
Button {
addGrandchild()
} label: {
Text("Add Grandchild Item")
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
}
}
private var grandchildrenViewSection: some View {
List {
ForEach(selectedChild.grandChildren) { grandchild in
Text(grandchild.name)
}
.onDelete(perform: deleteGrandchild)
}
.toolbar {
Button("Add Child", systemImage: "person.3.fill") {
addGrandchild()
}
}
.navigationTitle("Grandchildren View")
}
// MARK: - Functions
private func addGrandchild() {
let grandchild = Grandchild(name: UUID().uuidString, child: selectedChild)
// We are adding grandchild to child like this
selectedChild.grandChildren.append(grandchild)
}
private func deleteGrandchild(indexSet: IndexSet) {
for index in indexSet {
modelContext.delete(selectedChild.grandChildren[index])
}
}
}
// MARK: - Preview
#Preview {
let context = Parent.preview.mainContext
let parent = Parent(name: "Parent One")
let child = Child(name: "Child One", parent: parent)
let grandchild = Grandchild(name: "Grandchild One", child: child)
context.insert(grandchild)
return NavigationStack {
GrandchildView(selectedChild: child)
}
}