ModelActors not persisting relationships in iOS 18 beta

I've already submitted this as a bug report to Apple, but I am posting here so others can save themselves some troubleshooting. This is submitted as FB14337982 with an attached complete Xcode project to replicate.

In iOS 17 we use a ModelActor to download data which is saved as an Event, and then save it to SwiftData with a relationship to a Location. In iOS 18 22A5307d we are seeing that this code no longer persists the relationship to the Location, but still saves the Event. If we put a breakpoint in that ModelActor we see that the object graph is correct within the ModelActor stack trace at the time we call modelContext.save(). However, after saving, the relationship is missing from the default.store SQLite file, and of course from the app UI.

Here is a toy example showing how inserting an Employee into a Company using a ModelActor gives unexpected results in iOS 18 22A5307d but works as expected in iOS 17.

It appears that no relationships data survives being saved in a ModelActor.ModelContext.

Also note there seems to be a return of the old bug that saving this data in the ModelActor does not update the @Query in the UI in iOS 18 but does so in iOS 17.

Models

@Model
final class Employee {
    var uuid: UUID = UUID()
    @Relationship(deleteRule: .nullify) public var company: Company?

    /// For a concise display
    @Transient var name: String {
        self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
    }
    
    init(company: Company?) {
        self.company = company
    }
}

@Model
final class Company {
    var uuid: UUID = UUID()
    
    @Relationship(deleteRule: .cascade, inverse: \Employee.company)
    public var employees: [Employee]? = []

    /// For a concise display
    @Transient var name: String {
        self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
    }

    init() { }
}

ModelActor

import OSLog

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SimpleModelActor")

@ModelActor
final actor SimpleModelActor {
    func addEmployeeTo(CompanyWithID companyID: PersistentIdentifier?) {
        guard let companyID,
              let company: Company = self[companyID, as: Company.self] else {
            logger.error("Could not get a company")
            return
        }
            
        let newEmployee = Employee(company: company)
        modelContext.insert(newEmployee)
        logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
        try! modelContext.save()
    }
}

ContentView

import OSLog

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "View")

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var companies: [Company]
    @Query private var employees: [Employee]
    @State private var simpleModelActor: SimpleModelActor!

    var body: some View {
        ScrollView {
            LazyVStack {
                DisclosureGroup("Instructions") {
                    Text("""
                    Instructions:
                    1. In iOS 17, tap Add in View. Observe that an employee is added with Company matching the shown company name.
                    2. In iOS 18 beta (22A5307d), tap Add in ModelActor. Note that the View does not update (bug 1). Note in the XCode console that an Employee was created with a relationship to a Company.
                    3. Open the default.store SQLite file and observe that the created Employee does not have a Company relationship (bug 2). The relationship was not saved.
                    4. Tap Add in View. The same code is now executed in a Button closure. Note in the XCode console again that an Employee was created with a relationship to a Company. The View now updates showing both the previously created Employee with NIL company, and the View-created employee with the expected company.
                    """)
                    .font(.footnote)
                }
                .padding()
                Section("**Companies**") {
                    ForEach(companies) { company in
                        Text(company.name)
                    }
                }
                .padding(.bottom)
                
                Section("**Employees**") {
                    ForEach(employees) { employee in
                        Text("Employee \(employee.name) in company \(employee.company?.name ?? "NIL")")
                    }
                }
                Button("Add in View") {
                    let newEmployee = Employee(company: companies.first)
                    modelContext.insert(newEmployee)
                    logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
                    try! modelContext.save()
                }
                .buttonStyle(.bordered)

                Button("Add in ModelActor") {
                    Task {
                        await simpleModelActor.addEmployeeTo(CompanyWithID: companies.first?.persistentModelID)
                    }
                }
                .buttonStyle(.bordered)
            }
        }
        .onAppear {
            simpleModelActor = SimpleModelActor(modelContainer: modelContext.container)
            if companies.isEmpty {
                let newCompany = Company()
                modelContext.insert(newCompany)
                try! modelContext.save()
            }
        }
    }
}
Answered by rkhamilton in 799985022

This bug is fixed in the iOS 18.1 build in Xcode 16.1 beta 1, iOS 18.1 (22B5023b). This iOS build also fixes the bug where ModelActor context saves were not triggering view updates.

Updating because this bug is still present in Xcode 16 beta 4 in iOS 18 beta (22A5316f). I'm not aware of a workaround.

I am having a similar issue. One thing with your sample code there. If you create the model, insert it into the context, then add the relationships, the relationship will stay. so

let newEmployee = Employee()
modelContext.insert(newEmployee)
newEmloyee.company = companies.first
try! modelContext.save()

But even with this change the view is not updating

Hi @sendtobo I'm not seeing any change in behavior when I try your code. I added the following function to my example ModelActor:

    func createEmployeeThenAdd(CompanyWithID companyID: PersistentIdentifier?) {
        guard let companyID,
              let company: Company = self[companyID, as: Company.self] else {
            logger.error("Could not get a company")
            return
        }
            
        let newEmployee = Employee(company: nil)
        modelContext.insert(newEmployee)
        newEmployee.company = company
        logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
        try! modelContext.save()
    }

When I run it from iOS 17, everything works normally. When I run it from iOS 18 beta 4, I see in the console the expected result with a relationship:

Created employee 14E7161E in Company E3AD3F67

But the UI doesn't update (as you noted). However when I tap the "Add in View" button which runs on the main actor, I see the view update with both inserts (failed insert via ModelActor, successful via UI/main actor).

Employee 14E7161E in company NIL

Employee 81AA7153 in company E3AD3F67

I note in your example that you are adding companies.first. This means you are not getting the company to add in the same way as my example. I'm passing in the PersistentIdentifier and pulling the model object from the container with that reference, so my function doesn't have access to all companies. How are you getting the list of all companies to add one to the relationship in your example?

Unfortunately this bug is not fixed in Xcode 16 beta 5 / iOS 18.0 (22A5326g) beta.

Correct me if I'm wrong, but doesn't this break the only "correct" way to do background work that appends new ModelObjects to an existing to-many relationship?

Our app downloads data and creates new Items that populate an existing to-many relationship in a Location ModelObject. Our understanding is that the only idiomatic way to do this in the background with Swift Data is with a ModelActor, so this is how our app functions.

It would be great for a DTS Engineer to confirm that what I describe above is the correct approach, and that the bug described in this post is expected to be fixed before iOS 18 RC. Otherwise our app requires some more significant rework to get ready for iOS 18, and it's not clear that there is another Swift-Data-intended way to accomplish this task.

Accepted Answer

This bug is fixed in the iOS 18.1 build in Xcode 16.1 beta 1, iOS 18.1 (22B5023b). This iOS build also fixes the bug where ModelActor context saves were not triggering view updates.

This is not fixed on the iOS 18.1

Yes this issue still exits in iOS 18.1. Any EAT for fix from Apple?

This is utterly unacceptable Apple.

Are you actively trying to dissuade people from trusting any of frameworks... How is this not the top priority to fix?

I have a completely broken production application, which works perfectly fine on iOS 17, because it seems you are incapable of doing your jobs.

ModelActors not persisting relationships in iOS 18 beta
 
 
Q