UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Is its SwiftData or is it Me?

Forums > SwiftUI

I have had anissue with a re-implementation of an app using SwiftData. I finally reproduced it in a simple adaptation of Apple's boilerplate SwiftData app so either there is a bug in SwiftData, or, more likely, I am not understanding fully contexts and scope...

The issue revolves around putting methods and computed vars in the data model (classes). This code uses a computed var to count the number of SubItems in an Item, and a method to add a new SubItem.

The view that shows the subItems demonstrates that the method is not getting the updated number of SubItems. Restarting the App sets the count correctly. The image shows the result of two runs of the App. ![https://share.icloud.com/photos/001U6S7ve3MkqPpLTDTXDbYQQ]

import Foundation
import SwiftData

@Model
final class Item {
    @Relationship(inverse: \SubItem.item) var subItems : [SubItem]?
    var timestamp: Date
    var numOfSubitems : Int {
        subItems?.count ?? 0
    }

    init(timestamp: Date) {
        self.timestamp = timestamp
    }
    func addSubItem(context: ModelContext) {
        let newNum = numOfSubitems + 1
        let newSubItem = SubItem(item: self, subItemNumber: newNum)
        context.insert(newSubItem)
    }
}

@Model
final class SubItem {
    var item : Item
    var subItemNumber : Int

    init(item: Item, subItemNumber: Int = 0) {
        self.item = item
        self.subItemNumber = subItemNumber
    }
}
import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        ItemView(thisItem: item)
//                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
                    } label: {
                        HStack {
                            Text("SubItems: \(item.numOfSubitems)")
                            Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                        }
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}
import SwiftUI
import SwiftData

struct ItemView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var subItems: [SubItem]
    @Bindable var thisItem : Item

    init(thisItem: Item) {
        let id = thisItem.persistentModelID
        let predicate = #Predicate<SubItem> { sub in
            sub.item.persistentModelID == id
        }
        _subItems = Query(filter: predicate)
        self.thisItem = thisItem
    }

    var body: some View {
        VStack {
            Text(thisItem.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
            List {
                ForEach(subItems) { subitem in
                    Text("Subitem: \(subitem.subItemNumber)")
                }
            }
            .toolbar {

                ToolbarItem {
                    Button("Add Subitem") {
                        thisItem.addSubItem(context: modelContext)
                    }
                }
            }
        }
    }
}

   

hi,

i would change three things:

  • deep-six the query to find all the subitems of a given item; you already have them
  • explicitly identify the @Relationships for the model (i still don't trust SwiftData to identify these)
  • don't initialize a SubItem and set the value of its Item within the initializer prior to inserting the SubItem into the modelContext
  • (well, there's a fourth thing) ... i made sure both ends of the relationship were marked as optionals

so here's my simplification of Item.addSubItem()

    func addSubItem(context: ModelContext) {
        let newNum = numOfSubitems + 1
        let newSubItem = SubItem(subItemNumber: newNum)
        context.insert(newSubItem)
        subItems?.append(newSubItem)
    }

change to SubItem:

@Model
final class SubItem {
    @Relationship(deleteRule: .nullify)
    var item : Item? // now optional
    var subItemNumber : Int

    init(subItemNumber: Int = 0) {
        self.subItemNumber = subItemNumber
    }
}

and final version of ItemView:

struct ItemView: View {
    @Environment(\.modelContext) private var modelContext
    var thisItem : Item

  private var subItems: [SubItem] {
    // get local subItems array here with optional chaining plus sorting
        // there's no need to use a Query to find the subItems
        (thisItem.subItems ?? []).sorted(by: { $0.subItemNumber < $1.subItemNumber })
  }

    var body: some View {
        // no change to the body 
  }
}

i think i got all the highlights here ... let me know if i have missed anything and i'll put up the full code for you.

hope that helps,

DMG

   

Thanks DMG. That worked. Now I need to understand why! Did the extra @Query create a new "context?" that was updated separately from that used in ContentView?

BTW I thought it was unwise to specify both sides of a relationship with @Relationship? I think it was in Paul's SwiftData that he said specifying the inverse was a good idea in all cases, but only on one side.

Gerry

   

hi Gerry,

(1) i think the extra query was simply not necessary, because the subItems of an Item are already known when you enter the ItemView. i'd regard that as just a simplification.

(2) i think the major issue was Item.addSubItem(), which created a SubItem, but never actually appended it to the Item's subItems "array" ... which is really a Set, but i digress.

(3) i think the sequence of linking a new SubItem to an Item should always look like this:

let subItem = SubItem()
modelContext.insert(subItem)
item.subItems.append(subItem)

i think it makes clear to the Observation framework that there's a dependency here that needs to be obseerved. at least in the early betas, i think this alternative linking sequence (well known to Core Data users)

let subItem = SubItem()
modelContext.insert(subItem)
subItem.item = item

just did not make an Item responsive to changes in the subitems.

perhaps this will change in the future.

(4) as for both sides of the relationship, you certainly cannot specify each as the inverse: of the other; i just meant that you should decorate each end of the relationship with the @Relationship macro, and i think you should always specify a deleteRule. i think SwiftData really does need this help to do the right thing.

hope that helps,

DMG

   

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.