最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - SwiftData Custom Migration Always Fails - Possible Bug in SwiftData - Stack Overflow

programmeradmin3浏览0评论

I'm using SwiftData in my SwiftUI iOS app, and I need to implement a MigrationPlan before launching, so things go smoothly later when I need to make changes to the data being saved.

For example, without a migration in place, if I change the type of a property from Bool to [Bool], the app will crash when it tries to load SwiftData, since the saved model doesn't have the right type.


Current Setup

The current @Model, which is fairly simple by SwiftData standards, except that it has many more properties than I've included here:

@Model
final class MyData
{
    var property1: Bool = true
    var property2: Bool = true
    var property3: Int = 1
    // And many more properties...

    // Default init.
    init()
    {
        // Assign default values to properties.
        self.property1 = true
        self.property2 = true
        self.property3 = 1
        // ...
    }

    // Dynamic init.
    init(
        property1: Bool,
        property2: Bool,
        property3: Int
    )
    {
        self.property1 = property1
        self.property2 = property2
        self.property3 = property3
        // ...
    }   
}

Creation of the ModelContainer:

var sharedModelContainer: ModelContainer =
{
    let schema = Schema(
    [
        MyData.self,
    ])
    
    let modelConfiguration = ModelConfiguration(
        schema:                 schema,
        isStoredInMemoryOnly:   false
    )

    do
    {
        return try ModelContainer(
            for:                schema,
            configurations:     [modelConfiguration]
        )
    }
    catch
    {
        fatalError("Could not create ModelContainer: \(error)")
    }
}()

@main
struct myApp: App
{
    var body: some Scene
    {
        WindowGroup
        {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

Migration Attempt

To begin implementing the migration, I renamed the model to reflect its version:

@Model
final class MyDataV1
{
    // No other changes compared to the code shared above.
}

Then I defined MyDataV2 to represent the new format of the data. The only thing that has changed is the type of property1:

@Model
final class MyDataV2
{
    // Changed to an array.
    var property1: [Bool] = [true]
    var property2: Bool = true
    var property3: Int = 1
    // And many more properties...

    // Migration init.
    init(
        myDataV1: MyDataV1
    )
    {
        // Convert to an array.
        self.property1 = [myDataV1.property1]
        self.property2 = myDataV1.property2
        self.property3 = myDataV1.property3
        // ...
    }


    // Default init.
    init()
    {
        // Assign default values to properties.
        self.property1 = [true]
        self.property2 = true
        self.property3 = 1
        // ...
    }

    // Dynamic init.
    init(
        property1: [Bool],
        property2: Bool,
        property3: Int
    )
    {
        self.property1 = property1
        self.property2 = property2
        self.property3 = property3
        // ...
    }   
}

I defined a typealias to reflect the current version, so other parts of my app don't need to keep updating their references:

typealias MyData = MyDataV2

I defined two VersionedSchema instances, one to represent V1 and another to represent V2:

enum MyDataSchemaV1: VersionedSchema
{
    static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
    
    static var models: [any PersistentModel.Type]
    {
        [MyDataV1.self]
    }
}

enum MyDataSchemaV2: VersionedSchema
{
    static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
    
    static var models: [any PersistentModel.Type]
    {
        [MyDataV2.self]
    }
}

I defined a SchemaMigrationPlan to handle migrating from MyDataV1 to MyDataV2:

enum MyDataMigrationPlan: SchemaMigrationPlan
{
    static var schemas: [any VersionedSchema.Type]
    {
        [MyDataSchemaV1.self, MyDataSchemaV2.self]
    }
    
    static var stages: [MigrationStage]
    {
        [migrateFromV1ToV2]
    }
    
    private static var migrationFromV1ToV2Data: [MyDataV1] = []
    
    static let migrateFromV1ToV2 = MigrationStage.custom(
        fromVersion:    MyDataSchemaV1.self,
        toVersion:      MyDataSchemaV2.self,
        willMigrate:
        {
            modelContext in
            
            let descriptor  : FetchDescriptor<MyDataV1>    = FetchDescriptor<MyDataV1>()
            let v1Data      : [MyDataV1]                   = try modelContext.fetch(descriptor)
            
            migrationFromV1ToV2Data = v1Data
            
            try modelContext.delete(model: MyDataV1.self)
            try modelContext.save()
        },
        didMigrate:
        {
            modelContext in

            migrationFromV1ToV2Data.forEach
            {
                myDataV1 in
                
                let myDataV2 = MyDataV2(myDataV1: myDataV1)
                modelContext.insert(myDataV2)
            }
            
            try modelContext.save()
        }
    )
}

And finally I updated the ModelContainer initialization to include the migration plan:

var sharedModelContainer: ModelContainer =
{
    let schema = Schema(
    [
        MyData.self,
    ])
    
    let modelConfiguration = ModelConfiguration(
        schema:                 schema,
        isStoredInMemoryOnly:   false
    )

    do
    {
        return try ModelContainer(
            for:                schema,
            migrationPlan:      MyDataMigrationPlan.self,
            configurations:     [modelConfiguration]
        )
    }
    catch
    {
        fatalError("Could not create ModelContainer: \(error)")
    }
}()

Migration Errors

I created a situation in which an instance of MyDataV1 has already been saved to SwiftData, then I implemented the changes above and launched a new version.

I added logs to the MigrationPlan and confirmed that willMigrate starts and finishes without any errors, but the below errors show up before didMigrate even starts, and cause the app to crash:

CoreData: error: Error: Persistent History (3178) has to be truncated due to the following entities being removed: (
    MyDataV1
)
CoreData: warning: Warning: Dropping Indexes for Persistent History
CoreData: warning: Warning: Dropping Transactions prior to 3178 for Persistent History
CoreData: warning: Warning: Dropping Changes prior to TransactionID 3178 for Persistent History
CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134060)
CoreData: error: userInfo:
CoreData: error: NSLocalizedFailureReason : Instances of NSCloudKitMirroringDelegate are not reusable and should have a lifecycle tied to a given instance of NSPersistentStore.
CoreData: error: storeType: SQLite
CoreData: error: configuration: default

Attempted Solution 1

I thought that this might be because in willMigrate I was calling the following:

try modelContext.delete(model: MyDataV1.self)
try modelContext.save()

So I tried removing those lines, but it still caused the same error. This answer has the same implementation with supposedly no errors, so it's not surprising that this wasn't the cause of the issue.


Attempted Solution 2

This answer suggests that the later versions of the schema need to include the previous versions of the model in its models array. So, I updated the following:

enum MyDataSchemaV2: VersionedSchema
{
    static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
    
    static var models: [any PersistentModel.Type]
    {
        [MyDataV1.self, MyDataV2.self]
    }
}

This doesn't cause the same error, but still crashes with the following error before willMigrate even begins:

CoreData: error: Attempting to retrieve an NSManagedObjectModel version checksum while the model is still editable. This may result in an unstable verison checksum. Add model to NSPersistentStoreCoordinator and try again.

CoreData: debug: CoreData+CloudKit: -[PFCloudKitOptionsValidator validateOptions:andStoreOptions:error:](36): Validating options: <NSCloudKitMirroringDelegateOptions: 0x600003308960> containerIdentifier:iCloud.myapp.production databaseScope:Private ckAssetThresholdBytes:<null> operationMemoryThresholdBytes:<null> useEncryptedStorage:NO useDeviceToDeviceEncryption:NO automaticallyDownloadFileBackedFutures:NO automaticallyScheduleImportAndExportOperations:YES skipCloudKitSetup:NO preserveLegacyRecordMetadataBehavior:NO useDaemon:YES apsConnectionMachServiceName:<null> containerProvider:<PFCloudKitContainerProvider: 0x600000028280> storeMonitorProvider:<PFCloudKitStoreMonitorProvider: 0x6000000282b0> metricsClient:<PFCloudKitMetricsClient: 0x6000000282c0> metadataPurger:<PFCloudKitMetadataPurger: 0x6000000282d0> scheduler:<null> notificationListener:<null> containerOptions:<null> defaultOperationConfiguration:<null> progressProvider:<NSPersistentCloudKitContainer: 0x6000017af7c0> test_useLegacySavePolicy:YES archivingUtilities:<PFCloudKitArchivingUtilities: 0x6000000282e0> bypassSchedulerActivityForInitialImport:NO bypassDasdRateLimiting:NO activityVouchers:()

storeOptions: {
    NSInferMappingModelAutomaticallyOption = 1;
    NSMigratePersistentStoresAutomaticallyOption = 1;
    NSPersistentCloudKitContainerOptionsKey = "<NSPersistentCloudKitContainerOptions: 0x600002616580>";
    NSPersistentHistoryTrackingKey = 1;
    NSPersistentStoreMirroringOptionsKey =     {
        NSPersistentStoreMirroringDelegateOptionKey = "<NSCloudKitMirroringDelegate: 0x600003d3f1b0>";
    };
    NSPersistentStoreRemoteChangeNotificationOptionKey = 1;
    NSPersistentStoreStagedMigrationManagerOptionKey = "<NSStagedMigrationManager: 0x6000002ac6a0>";
}

CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134504)
CoreData: error: userInfo:
CoreData: error:    NSLocalizedDescription : Cannot use staged migration with an unknown coordinator model version.
CoreData: error: storeType: SQLite
CoreData: error: configuration: default

Where I think this line is the relevant error:

Cannot use staged migration with an unknown coordinator model version.

This answer suggests that the issue is that SwiftData doesn't know from which model to begin migrating. I don't know how that could be the case, or if it is the case, how I could fix it.


Attempted Solution 3

Then, I tried initializing ModelContainer by giving it both versions, not just the most recent one:

var sharedModelContainer: ModelContainer =
{
    let schema = Schema(
    [
        MyDataV1.self,
        MyDataV2.self
    ])
    
    let modelConfiguration = ModelConfiguration(
        schema:                 schema,
        isStoredInMemoryOnly:   false
    )

    do
    {
        return try ModelContainer(
            for:                schema,
            migrationPlan:      MyDataMigrationPlan.self,
            configurations:     [modelConfiguration]
        )
    }
    catch
    {
        fatalError("Could not create ModelContainer: \(error)")
    }
}()

While still excluding these two lines from willMigrate:

try modelContext.delete(model: MyDataV1.self)
try modelContext.save()

This time, it finished willMigrate (like in the earliest attempt), but still crashed with a new error immediately after willMigrate finished:

CoreData: CloudKit: CoreData+CloudKit: -[PFCloudKitStoreMonitor pfcloudstoremonitor_is_holding_your_store_open_waiting_for_cloudkit_activity_to_finish](125): <PFCloudKitStoreMonitor: 0x60000179b140>: Exporter / importer finished after 1 tries. Allowing store to deallocate.

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1240): <NSCloudKitMirroringDelegate: 0x600003d47570>: Failed to set up CloudKit integration for store: <NSSQLCore: 0x10201d870>
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2310): <NSCloudKitMirroringDelegate: 0x600003d47570> - Attempting recovery from error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:]_block_invoke(2329): The store was removed before the mirroring delegate could recover from an error:
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _finishedRequest:withResult:](3582): Finished request: <NSCloudKitMirroringDelegateSetupRequest: 0x60000213bf70> 9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA with result: <NSCloudKitMirroringResult: 0x600000c97c60> storeIdentifier: 2FE6E954-8AAF-4D89-8C6C-741C890ADEFC success: 0 madeChanges: 0 error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](3551): <NSCloudKitMirroringDelegate: 0x600003d47570>: Checking for pending requests.

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1302): Failed to finish setup event: Error Domain=NSCocoaErrorDomain Code=134407 "Request '9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA' was cancelled because the store was removed from the coordinator." UserInfo={NSLocalizedFailureReason=Request '9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA' was cancelled because the store was removed from the coordinator.}
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate observeChangesForStore:inPersistentStoreCoordinator:](427): <NSCloudKitMirroringDelegate: 0x600003d07de0>: Observing store: <NSSQLCore: 0x10201f7b0>
BUG IN CLIENT OF CLOUDKIT: Registering a handler for a CKScheduler activity identifier that has already been registered (com.apple.coredata.cloudkit.activity.export.2FE6E954-8AAF-4D89-8C6C-741C890ADEFC).

Where this may be the relevant error:

Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}

And about 15 lines like this were logged during willMigrate:

CoreData: debug: CoreData+CloudKit: -[PFCloudKitMetadataModelMigrator calculateMigrationStepsWithConnection:error:](450): Skipping migration for 'ANSCKDATABASEMETADATA' because it already has a column named 'ZLASTFETCHDATE'

Is it a CoreData/SwiftData bug?

This Apple DTS Engineer about 3 weeks ago suggested that these exact errors I'm getting in my Attempted Solution 3 section are framework bugs.

So, what does this mean? SwiftData migration just doesn't work at all if you've also enabled CloudKit sync?


How can I get this working?

发布评论

评论列表(0)

  1. 暂无评论