Sending Core Data models to CloudKit

To send Core Data models to CloudKit, you must convert them to CKRecord instances. So far, you've learned how to create an instance of CKRecord and you even implemented a helper method on the Movie and FamilyMember objects. Take another look at recordForZone(_:) on both objects to make sure you understand how the models are converted to records. When you convert Core Data objects to CloudKit models, it's important that you properly use the CloudKit metadata. If you attempt to store an existing object with the correct record name, but you omit the metadata, CloudKit won't be able to save your object because it can't accurately compare the version that it currently has stored and the new version you are trying to send it.

Before you update the storeFamilyMember(_:_) method, update the saveMovie(withName:) method in MoviesViewController so family members get stored when you add a new family member or when you update one by assigning it a new movie. Update the method as follows:

func saveMovie(withName name: String) {
  // ...

    let helper = MovieDBHelper()
    helper.fetchRating(forMovie: name) { remoteId, rating in
      guard let rating = rating,
        let remoteId = remoteId
        else { return }

      moc.persist {
        movie.popularity = rating
        movie.remoteId = Int64(remoteId)

        self.cloudStore.storeFamilyMember(familyMember) { _ in
          // no action
        }
      }
  // ...
}

The only thing that has changed in this method is that the family member is now stored after the movie's popularity rating is fetched. The next and final step is to implement the new version of storeFamilyMember(_:_), as follows:

func storeFamilyMember(_ familyMember: FamilyMember, _ completionHandler: @escaping (Error?) -> Void) {
  // 1
  guard let movies = familyMember.movies as? Set<Movie> else {
    completionHandler(nil)
    return
  }

  let defaultZoneId = CKRecordZone.ID(zoneName: "moviesZone", ownerName: CKCurrentUserDefaultName)

  // 2
  var recordsToSave = movies.map { movie in
    movie.recordForZone(defaultZoneId)
  }
  recordsToSave.append(familyMember.recordForZone(defaultZoneId))

  let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil)

  operation.modifyRecordsCompletionBlock = { records, recordIds, error in

    guard let records = records, error == nil else {
      completionHandler(error)
      return
    }

    // 3
    for record in records {
      if record.recordType == "FamilyMember" {
        familyMember.managedObjectContext?.persist {
          familyMember.cloudKitData = record.encodedSystemFields
        }
      } else if record.recordType == "Movie",
        let movie = movies.first(where: { $0.recordName == record.recordID.recordName }) {

        familyMember.managedObjectContext?.persist {
          movie.cloudKitData = record.encodedSystemFields
        }
      }
    }

    completionHandler(error)
  }

  privateDatabase.add(operation)
}

Even though the preceding method is quite long, it should be relatively straightforward; there are some parts that are worth taking a closer look at:

  1. Because the movies property on FamilyMember is an NSSet, it needs to be converted to a Set<Movie> so the movies can be looped over. This conversion should never fail, so if it does, something is wrong and it makes no sense to continue saving the family member to CloudKit.
  2. Extract all movie records from the family member to obtain a list of records that should be saved. The family member itself is then also added to this list, so it's also saved to CloudKit.
  1. Once the save operation is created, and all records are saved, the CloudKit metadata is added to their corresponding Core Data objects, and the objects are then saved. Note that the managed object context is obtained by calling familyMember.managedObjectContext. Doing this ensures that the family member and its movies are updated in the correct managed object context.

If you run your app now, you should be able to import records from CloudKit automatically. When you add records to CloudKit while the app is running, it will automatically update the user interface. And when you add new family members and movies to the Core Data database, they are automatically saved to CloudKit.

If you want to test your app on multiple devices at once, you can use different simulators that are signed in to the same iCloud account. When you add a family member on one simulator, it will automatically appear on all other simulators that have the same iCloud user.