SwiftData regression in iOS 18 RC, contexts don't sync

In iOS 18 RC, and the iOS 18 simulator shipped with Xcode 16.0 RC, there is a regression where ModelContexts on the same ModelContainer do not sync changes. A minimal example is below, but briefly: create an object in context1. Retrieve and update that object in context2, then save context2. The changes cannot be found in context1 in iOS 18 RC, but can in iOS 17 and earlier betas of iOS 18.

I've submitted this as FB15092827 but am posting here for visibility to others. I'm going to have to scramble to see if I can mitigate this in our impacted app before iOS 18 launches. It's affecting us when doing background inserts in a ModelActor to populate our app UI, but you can see below the effects are seen even on the same thread in a very simple two-context example.

    @Test("updates sync between contexts") func crossContextSync() async throws {
        // overview:
        // create an employee in context 1
        // update the employee in context 2
        // check that the update is available in context 1
        
        let context1 = ModelContext(demoAppContainer)
        let context2 = ModelContext(demoAppContainer)

        // create an employee in context 1
        let newEmployee = Employee(salary: 0)
        context1.insert(newEmployee)
        try context1.save()
        #expect(newEmployee.salary == 0, "Created with salary 0")
        
        // update the employee in context 2
        let employeeID = newEmployee.uuid
        let predicate: Predicate<Employee> = #Predicate<Employee> { employee in
            employee.uuid == employeeID
        }
        let fetchedEmployee = try #require(try? context2.fetch(FetchDescriptor<Employee>(predicate: predicate)).first)
        #expect(fetchedEmployee.uuid == newEmployee.uuid, "We got the correct employee in the new context")
        let updatedSalary = 1
        fetchedEmployee.salary = updatedSalary
        try context2.save()

        // FAILURE IS HERE. This passes in earlier iOS betas and in iOS 17.X
        #expect(newEmployee.salary == updatedSalary, "Salary was update in context 1")

        // Create a new modelContext on the same container, since the container does have the changes in it.
        // By creating this new context we can get updated data and the test below passes in all iOS versions tested. This may be a mitigation path but creating new contexts any time you need to access data is painful.
        let context3 = ModelContext(demoAppContainer)
        let fetchedEmployeeIn3 = try #require(try? context3.fetch(FetchDescriptor<Employee>(predicate: predicate)).first)
        #expect(fetchedEmployeeIn3.uuid == newEmployee.uuid, "We got the correct employee in the new context3")
        #expect(fetchedEmployeeIn3.salary == updatedSalary, "Salary was update in context 1")
    }

Code below if you want to build a working example, but the test above is very simple

let demoAppContainer = try! ModelContainer(for: Employee.self)

@main
struct ModelContextsNotSyncedToContainerApp: App {
    init() {
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(demoAppContainer)
        }
    }
}

@Model
final class Employee {
    var uuid: UUID = UUID()
    var salary: Int
    
    init(salary: Int = 0) {
        self.salary = salary
    }
}

It turned out that we could get this fixed pretty quickly in our app, but there was more to fix in our unit tests. A pattern we had adopted in unit tests was testing async operations performed by our ModelActors:

  1. Create a ModelContext in a unit test.
  2. Create a ModelObject instance in that unit test Context
  3. Send that ModelObject to a ModelActor to get updated
  4. Back in the unit test, confirm that the ModelObject was updated.

It's that step 4 that was broken by this change in iOS 18 RC.

Thanks for filing the feedback report. The issue seems to be similar to the one discussed here, but in a simpler use case.

You mentioned that you had fixed the issue by creating a new context. I am just curious – Does the issue impact your app in any other way? For example, if you inject yourModelContianer.mainContext into SwiftUI environment as viewContext, does viewContext update for you when you make changes via the other model context?

Also, I tried with my iOS 17 device and saw the issue there as well, and so am wondering how you confirmed iOS 17 and earlier betas of iOS 18 don't have the issue.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for your reply.

I can't explain why you saw the unit test pass. I see passes in the simulator in Xcode 16 RC using iOS 17.4 and 17.5. It fails for me only in iOS 18 RC on the simulator. The full demo project is attached to the FB linked above if that is helpful. If I can provide any other diagnostics please let me know.

We developed the app in iOS 17.0 and these unit tests passed all year in all point releases of iOS 17. We were also all green for later iOS 18 betas prior to the RC.

I can't say for certain that there is no other impact to our app from this bug. We saw a change in concurrency behavior in iOS 18 RC compared to the betas (and compared to iOS 17.X) which revealed a race condition in our code. We had reliably "won" the race prior to the RC, but reliably lost in the RC. We had to refactor a chunk of code to get that to work, and we may have also fixed any issues coming from the ModelContext behavior above at the same time.

The only app impact that seems like I can point specifically at this bug is in a view that took a ModelObject as an argument, rather than running its own @Query. We have two models

@Model Parent {
    var children: [Child]
}
@Model Child {}

The view below would continue to show the ContentUnavailableView even after our background process had updated the parent instance, and it's content was visible on other screens which were populated successfully with @Query variable.

MyView {
    let parent: Parent // <- passed as argument, not query
    var body {
        if let children = parent.children,
            children.isEmpty == false {
            trendsContent
        } else {
            ContentUnavailableView(...)
        }
    }
}

The issue you linked where Query vars do not update when ModelActors update is one we saw during all early betas of iOS 18 (and iOS 17.0 betas as well), but have not been impacted by in any released iOS version.

OK, I tried your demo project and did see that your test passed on my iOS 17.6.1 device. This is consistent with what you described.

I then copied your code, tweaked a bit by commenting out the Unit Test specific code ( #expect, etc), and ran it in ContentView.onAppear. I saw that the newEmployee.salary was not updated. This is consistent what I saw before.

I don't have time to look into the difference between the app environment and the unit test yet, and may revisit the topic later, but since now your test demos the issue in the Xcode 18 RC simulator, I'd probably wait a bit to see what the SwiftData folks have to say.

Lastly, just to clarify:

I can't explain why you saw the unit test pass.

Maybe there was a typo here, but to be clear, I did not see that your test passed in Xcode 18 RC simulator. I can reproduce what you described.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

SwiftData regression in iOS 18 RC, contexts don't sync
 
 
Q