In this article, we will discuss some common problems in developing Core Data with CloudKit projects.

Console logs

For a project that supports Core Data with CloudKit, the console output will normally be as shown above.

Each project faces a different situation and there is a lot of nonsense in the information, so I will just summarize the types of information possible.

Normal information

  • Initialization information

    Code starts, usually first appeared in the console is NSPersistentCloudKitContainer initialization information display. Including: success at the specified url to create a container, success enabled NSCloudKitMirroringDelegate synchronous response, etc. If you are running the project for the first time, there will be a reminder that you have successfully created a Schema on iCloud.

  • Data model migration information

    If the local and server-side data models are inconsistent, a migration alert will appear. Sometimes even if the native Core Data model is the same as the iCloud model, Skipping migration for ‘ANSCKDATABASEMETADATA’ because it already has a column named ‘ZLASTFETCHDATE’

  • Data synchronization information

    Will describe the specific content of import and export in detail, the information is easy to understand. Any data changes on the application side or the server side will show corresponding information.

  • Persist history trace information

    NSPersistentCloudKitContainer use persistent history tracking to administer the import and export business, the data synchronization information about often accompany contains NSPersistentHistoryToken such hints. Ignoring remote change notification because the exporter has already caught up to this transaction: 11/11 –

    information is also generated by persistent history tracing, which makes it easy to think that there is always a transaction that has not been processed. Read my other article on Persistent History Tracking using Persistent History Tracking in CoreData.

Information about possible irregularities

  • Initialization error

    Common examples include the failure to create or read sqLite files, resulting in local URL errors, and CKContainerID permission issues. If the URL points to appGroupContainer, ensure that appGroupID is correct and that the app has group permission. CKContainerID permission issues are usually resolved using the configuration in reset Certificates,Identifiers&Profiles mentioned in the previous article.

  • Model migration error

    Normally, Xcode won’t let you generate managed Object Models that aren’t compatible with CloudKit’s Schema, so most of the time, it’s because in a development environment, Problems caused by a mismatch between the local and server-side data models (such as changing an attribute name, or using an older development version, etc.). If the code version is correct, delete the local APP and reset the CloudKit development environment to solve the problem. However, if your application is already online, you should try to avoid such problems. Consider the model migration strategy provided by the Update data model later in this article.

  • Merge conflicts

    Please check whether to set up the correct NSMergeByPropertyObjectTrumpMergePolicy merge conflicts strategy? Did you make incorrect changes to the data from the CloudKit console? If it is still under development, it can be solved in the same way as above.

  • ICloud account or network error

    ICloud login, iCloud server not responding, iCloud account restricted, etc. Most of these problems are unsolvable on the developer side. NSPersistentCloudKitContainer after up to account login automatically restore synchronization. Check the account status in the code and remind the user to log in to the account.

Disabling Log Output

After confirming that the synchronization function code is working properly, you can try to disable the log output of Core Data with CloudKit if you cannot tolerate the barrage of messages from the console. To debug any project that uses Core Data, I recommend adding the following default parameters to your project:

  • -com.apple.CoreData.ConcurrencyDebug

    Detect problems caused by managed object or context thread errors in a timely manner. Any code that might cause an error is executed, and the application crashes immediately, helping to clean up the bug during development. When enabled, the console will display CoreData: Annotation: CoreData multi-threading assertions enabled.

  • -com.apple.CoreData.CloudKitDebug

    CloudKit Indicates the output level of debugging information. The value starts from 1. A larger number indicates more detailed information

  • -com.apple.CoreData.SQLDebug

    The actual SQL statement that CoreData sends to SQLite, 1 — 4, the larger the number, the more detailed. The output provides information that can be useful when debugging performance problems — in particular, it can tell you when Core Data is performing a lot of small extracts (such as when faults are populated separately).

  • -com.apple.CoreData.MigrationDebug

    The migration debug startup parameter will enable you to see exceptions in the console when migrating data.

  • -com.apple.CoreData.Logging.stderr

    Information output switch

Settings – com. Apple. CoreData. Logging. Stderr 0, all related to database log information will no longer be output.

Disabling network synchronization

During program development, we sometimes don’t want to be bothered by data synchronization. Add network synchronization control parameters to improve concentration.

When NSPersistentCloudKitContainer load without configuration cloudKitContainerOptions NSPersistentStoreDescription, Its behavior is consistent with that of NSPersistentContainer. You can control whether or not to enable data network synchronization during debugging by using code like the following.

let allowCloudKitSync: Bool = {
        let arguments = ProcessInfo.processInfo.arguments
        guard let index = arguments.firstIndex(where: {$0 = = "-allowCloudKitSync"}),
              index + 1 < arguments.count - 1 else {return true}
        return arguments[index + 1] = = "1"} ()if allowCloudKitSync {
            cloudDesc.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.fatobman.blog.SyncDemo")}else {
            cloudDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
            cloudDesc.setOption(true as NSNumber,
                                forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)}Copy the code

Because NSPersistentCloudKitContiner will automatically enable persistence history tracking, such as not set NSPersistentCloudKitContainerOptions, You must explicitly enable Persistent History Tracking in your code, otherwise the database becomes read-only.

Set this parameter to 0 to disable network synchronization.

Changes to the local database will still be synchronized to the server after synchronization is restored.

Abnormal synchronization

If network synchronization is abnormal, perform the following checks:

  • The network connection is normal
  • Whether the device is logged iniCloudaccount
  • Whether the devices that synchronize the private database are logged in to the same deviceiCloudaccount
  • Check logs for error messages, especially on the server
  • The emulator does not support silent push in the backgroundappSwitch to the background and switch back to see if there is any data

If you still can’t find the reason, please make a pot of tea, listen to a song, look into the distance, after a while may be ok.

Apple’s servers are a frequent source of leaks, so push delays shouldn’t come as a surprise.

Check the user account status

NSPersistentCloudKitContainer when up accounts available automatically restore network synchronization. The code checks the user’s iCloud account logins and alerts the user to the account logins in the app.

Call ckcontainer.default ().accountstatus to check the iCloud accountStatus of the user, subscribe to CKAccountChanged, and cancel the notification after successful login. Such as

    func checkAccountStatus(a) {
        CKContainer.default().accountStatus { status, error in
          DispatchQueue.main.async {
            switch status {
            case .available:
               accountAvailable = true
            default:
           	   accountAvailable = false
            }
            if let error = error {
                print(error)
            }
          }
        }
    }
Copy the code

Check the network synchronization status

CloudKit does not provide a detailed network synchronization status API, so developers do not have access to information such as how much data needs to be synchronized, how much synchronization is happening, and so on.

NSPersistentCloudKitContainer provides a eventChangedNotification notice, the notice will be in the import, export, setup three states when switching to remind us. Strictly speaking, it is difficult to judge the actual state of the current synchronization just by switching notifications.

In actual use, the data import state has the biggest impact on users’ perception. When a user installs an application on a new device and already has a lot of data stored on the network, it can be confusing to face an application with no data at all.

Data is imported 20-30 seconds after the application starts, and if the data volume is large, it is likely that the user will not see the data on the UI until 1-2 minutes later (bulk imports typically merge into the context after the entire batch has been imported). Therefore, it is important to provide users with adequate prompts.

In actual use, when the import state is finished, it will switch to another state. Try to give the user a little hint with code like this.

@State private var importing = false
@State private var publisher = NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification)

var body:some View{
  VStack{
     if importing {
        ProgressView()
     }
  }
  .onReceive(publisher){ notification in
     if let userInfo = notification.userInfo {
        if let event = userInfo["event"] as? NSPersistentCloudKitContainer.Event {
            if event.type = = .import {
              importing = true
            }
            else {
              importing = false
            }
         }
      }
   }  
}
Copy the code

When an application is switched back to the background, the synchronization continues for about 30 seconds. After the application is switched back to the foreground, data synchronization continues. Therefore, when there is a large amount of data, it is necessary to do a good job of prompting the user (such as keeping in the foreground, or letting the user continue to wait).

Create the default data set

Some applications provide the user with some default data, such as a starting dataset or a demonstration dataset. Care should be taken if the data set provided is placed in a synchronizable database. For example, a default data set has been created and modified on one device, and when the application is installed and run again on a new device, improper handling may result in data being overwritten or duplicated.

  • Verify that the data set must be synchronized

    If synchronization is not required, consider synchronizing the local database to the iCloud private database.

  • For example, the data set must be synchronized

    1. It is best to guide the user to manually click the create default data button and let the user decide whether to create it again.

    2. CKQuerySubscription can also be used when the application is first run to determine whether the data is in the network database by querying specific records (this method is used by a user who was not satisfied with the response a few days ago).

    3. Consider using NSUbiquitousKeyValueStore judging or permission.

Both methods need to ensure that the network and account status is normal before checking. It is probably easiest for users to make their own judgment.

Moving the local Database

Applications that are already in the AppStore, in some cases have a need to move local databases to other urls. For example, to make the Widget accessible to the database, I moved the health note database to the appGroupContainerURL.

If use NSPersistentContainer, you can directly call the coordinator. MigratePersistentStore the location of the database file transfer security. But if NSPersistentCloudKitContainer loaded store on this method is called, it must again after being forced to exit the application into can normal use (although the database file transferred, but after the migration will inform loading CloudKit container error, unable to synchronize. You need to restart the application to synchronize properly).

Therefore, the correct move scenario is to use FileManager to move the database file to the new location before creating the Container. You need to move the sqLite, SQLite-wal, and SQLite-shm files simultaneously.

Code like this:

func migrateStore(a) {
        let fm = FileManager.default
        guard !FileManager.default.fileExists(atPath: groupStoreURL.path) else {
            return
        }

        guard FileManager.default.fileExists(atPath: originalStoreURL.path) else {
            return
        }

        let walFileURL = originalStoreURL.deletingPathExtension().appendingPathExtension("sqlite-wal")
        let shmFileURL = originalStoreURL.deletingPathExtension().appendingPathExtension("sqlite-shm")
        let originalFileURLs = [originalStoreURL, walFileURL, shmFileURL]
        let targetFileURLs = originalFileURLs.map {
            groupStoreURL
                .deletingLastPathComponent()
                .appendingPathComponent($0.lastPathComponent)
        }

        // Move the original file to the new location.
        zip(originalFileURLs, targetFileURLs).forEach { originalURL, targetURL in
            do {
                try fm.moveItem(at: originalURL, to: targetURL)
            } catch error {
                print(error)
            }
        }
}
Copy the code

Update the data model

In the CloudKit Dashboard article, we’ve discussed two environments for CloudKit. Once the Schema is deployed to production, there is no way to rename or delete record types and fields. Your application must be carefully planned so that it remains forward compatible with updates to the data model.

Do not modify the data model as much as you want, and try to do as much as possible for entities and attributes: only add, do not subtract, do not change.

The following model update strategy can be considered:

Incremental updating

Add a record type incrementally or add a new field to an existing record type.

In this way, older versions of the application can still access the records created by the user, but not every field.

Make sure that the new attributes or entities only serve the new features of the new version, and that the new version of the program will work without the data (in this case, the user will still update the data with the old version, and the new entities and attributes will have no content).

Adding the version attribute

This strategy is an enhanced version of the previous one. The entity is versioning by initially adding the Version attribute to it, and the predicate extracts only the records that are compatible with the current version of the application. The old version of the program will not extract the data created by the new version.

For example, the entity Post has a version attribute

// The current data version.
let maxCompatibleVersion = 3

context.performAndWait {
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Post")
    
    // Extract data not larger than the current version
    fetchRequest.predicate = NSPredicate(
        format: "version <= %d",
        argumentArray: [maxCompatibleVersion]
    )
    
    let results = context.fetch(fetchRequest)
}
Copy the code

The data is locked, prompting the upgrade

With the Version attribute, an application can easily know that the current version no longer meets the needs of the data model. It can prevent the user from modifying data and prompt the user to update the application version.

Create a new CKContainer and a new local storage

If your data model has changed so dramatically that this is too difficult to handle, or if this is a huge waste of data, you can add a new associative container to your application and code to move the original data to the new container.

The general process is as follows:

  • Add new ones to the applicationxcdatamodeld(There should be two models, one for the old container and one for the new container.)
  • Adding a new associative container to the application (using both containers)
  • Determine if the migration has taken place and, if not, let the application work through the old model and container
  • Give the user the option to migrate the data (remind the user to ensure that the old data has been synchronized to the local before performing the migration)
  • The tag migration is complete by code moving the old data to the new container and local storage (use twoNSPersistentCloudKitContainer)
  • Switching data sources

Regardless of the above strategy, data loss and confusion should be avoided at all costs.

conclusion

The problems in this article are those I encountered and have tried to solve during development. Other developers will encounter more unknowns, and if they can understand the rules, they can always find a solution.

In the next article, we’ll talk about synchronizing public databases

This article originally appeared on my personal blog, Swift Notepad.