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
}
}