Sharing Core Data With CloudKit in SwiftUI
Learn to share data between CoreData and CloudKit in a SwiftUI app. By Marc Aupont.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sharing Core Data With CloudKit in SwiftUI
30 mins
- Getting Started
- Types of CloudKit Databases
- Enabling CloudKit Syncing
- CloudKit Console Dashboard
- Updating NSPersistentCloudKitContainer to Prepare for Share
- Presenting UICloudSharingController
- Accepting Share Invitations
- Fetching Shared Data
- Displaying Private Data Versus Shared Data
- Challenge Time
- Challenge One
- Challenge Two
- Where to Go From Here?
Updating NSPersistentCloudKitContainer to Prepare for Share
At this point, your app can locally persist your changes on the device while also syncing them with a private database in iCloud. However, to allow other users to interact with this data, you need to update your NSPersistentCloudKitContainer
. Open CoreDataStack.swift. The class contains all the necessary methods and properties you need to interact with Core Data. To begin the sharing process, add the following code to your persistentContainer
below the // TODO: 1
comment:
let sharedStoreURL = storesURL?.appendingPathComponent("shared.sqlite")
guard let sharedStoreDescription = privateStoreDescription
.copy() as? NSPersistentStoreDescription else {
fatalError(
"Copying the private store description returned an unexpected value."
)
}
sharedStoreDescription.url = sharedStoreURL
This code configures the shared database to store records shared with you. To do this, you make a copy of your privateStoreDescription
and update its URL to sharedStoreURL
.
Next, add the following code under the // TODO: 2
comment:
guard let containerIdentifier = privateStoreDescription
.cloudKitContainerOptions?.containerIdentifier else {
fatalError("Unable to get containerIdentifier")
}
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: containerIdentifier
)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
This code creates NSPersistentContainerCloudKitContainerOptions
, using the identifier from your private store description. In addition to this, you set databaseScope
to .shared
. The final step is to set the cloudKitContainerOptions
property for the sharedStoreDescription
you created.
Next, add the following code below the // TODO: 3
comment:
container.persistentStoreDescriptions.append(sharedStoreDescription)
This code adds your shared NSPersistentStoreDescription
to the container.
Last and under the // TODO: 4
, replace:
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Failed to load persistent stores: \(error)")
}
}
With the following:
container.loadPersistentStores { loadedStoreDescription, error in
if let error = error as NSError? {
fatalError("Failed to load persistent stores: \(error)")
} else if let cloudKitContainerOptions = loadedStoreDescription
.cloudKitContainerOptions {
guard let loadedStoreDescritionURL = loadedStoreDescription.url else {
return
}
if cloudKitContainerOptions.databaseScope == .private {
let privateStore = container.persistentStoreCoordinator
.persistentStore(for: loadedStoreDescritionURL)
self._privatePersistentStore = privateStore
} else if cloudKitContainerOptions.databaseScope == .shared {
let sharedStore = container.persistentStoreCoordinator
.persistentStore(for: loadedStoreDescritionURL)
self._sharedPersistentStore = sharedStore
}
}
}
The code above stores a reference to each store when it’s loaded. It checks databaseScope
and determines whether it’s private
or shared
. Then, it sets the persistent store based on the scope.
Presenting UICloudSharingController
The UICloudSharingController is a view controller that presents standard screens for adding and removing people from a CloudKit share record. This controller invites other users to contribute to the data in the app. There’s just one catch: This controller is a UIKit controller, and your app is SwiftUI.
The solution is in CloudSharingController.swift. CloudSharingView
conforms to the protocol UIViewControllerRepresentable
and wraps the UIKit
UICloudSharingController
so you can use it in SwiftUI. The CloudSharingView
has three properties:
- CKShare: The record type you use for sharing.
- CKContainer: The container that stores your private, shared or public databases.
- Destination: The entity that contains the data you’re sharing.
In makeUIViewController(context:)
, the following actions occur. It:
- Configures the title of the share. When
UICloudSharingController
presents to the user, they must have some context of the shared data. In this scenario, you use the caption of the destination. - Creates a
UICloudSharingController
using theshare
andcontainer
properties. The presentation style is set to.formSheet
and delegate is set using theCloudSharingCoordinator
. This is responsible for conforming to theUICloudSharingControllerDelegate
. This delegate contains the convenience methods that notify you when certain actions happen with the share, such as errors and sharing status. Now that you’re aware of howCloudSharingView
works, it’s time to connect it to your share button.
Now, open DestinationDetailView.swift. This view contains the logic for your share button. The first step is to create a method that prepares your shared data. To achieve this, iOS 15 introduces share(_:to:)
. Add the following code to the extension block:
private func createShare(_ destination: Destination) async {
do {
let (_, share, _) =
try await stack.persistentContainer.share([destination], to: nil)
share[CKShare.SystemFieldKey.title] = destination.caption
self.share = share
} catch {
print("Failed to create share")
}
}
The code above calls the async version of share(_:to:)
to share the destination you’ve selected. If there’s no error, the title of the share is set. From here, you store a reference to CKShare
that returns from the share method. You’ll use the default share when you present the CloudSharingView
.
Now that you have the method to perform the share, you need to present the CloudSharingView
when you tap the Share button. Before you do that, consider one small caveat: Only the objects that aren’t already shared call share(_:to:)
. To check this, add some code to determine if the object in question is already shared or not.
Back in CoreDataStack.swift, add the following extension:
extension CoreDataStack {
private func isShared(objectID: NSManagedObjectID) -> Bool {
var isShared = false
if let persistentStore = objectID.persistentStore {
if persistentStore == sharedPersistentStore {
isShared = true
} else {
let container = persistentContainer
do {
let shares = try container.fetchShares(matching: [objectID])
if shares.first != nil {
isShared = true
}
} catch {
print("Failed to fetch share for \(objectID): \(error)")
}
}
}
return isShared
}
}
This extension contains the code related to sharing. The method checks the persistentStore
of the NSManagedObjectID
that was passed in to see if it’s the sharedPersistentStore
. If it is, then this object is already shared. Otherwise, use fetchShares(matching:)
to see if you have objects matching the objectID in question. If a match returns, this object is already shared. Generally speaking, you’ll be working with an NSManagedObject
from your view.
Add the following method to your extension:
func isShared(object: NSManagedObject) -> Bool {
isShared(objectID: object.objectID)
}
With this code in place, you can determine if the destination is already shared and then take the proper action.
Add the following code to CoreDataStack
:
var ckContainer: CKContainer {
let storeDescription = persistentContainer.persistentStoreDescriptions.first
guard let identifier = storeDescription?
.cloudKitContainerOptions?.containerIdentifier else {
fatalError("Unable to get container identifier")
}
return CKContainer(identifier: identifier)
}
Here you created a CKContainer
property using your persistent container store description.
As you prepare to present your CloudSharingView
, you need this property because the second parameter of CloudSharingView
is a CKContainer
.
With this code in place, navigate back to DestinationDetailView.swift to present CloudSharingView
. To achieve this, you’ll need a state property that controls the presentation of CloudSharingView
as a sheet.
First, add the following property to DestinationDetailView
:
@State private var showShareSheet = false
Second, you need to add a sheet modifier to the List
to present CloudSharingView
. Add the following code, just above the existing sheet modifier:
.sheet(isPresented: $showShareSheet, content: {
if let share = share {
CloudSharingView(
share: share,
container: stack.ckContainer,
destination: destination
)
}
})
This code uses showShareSheet
to present the CloudSharingView
, when the Boolean is true
. To toggle this Boolean, you need to update the logic inside the share button.
Repace:
print("Share button tapped")
With:
if !stack.isShared(object: destination) {
Task {
await createShare(destination)
}
}
showShareSheet = true
This logic first checks to see whether the object is shared. If it’s not shared, create the share from the destination object. Once you’ve completed that task, set showShareSheet
, which presents CloudSharingView
, to true
. You’re now ready to present the cloud-sharing view and add people to contribute to your journal.
Log in to your iCloud account on a real device. Build and run. Add a destination. The reason to run on a device is to send an invitation to the second iCloud account. The most common options are via email or text message.
Once you add the destination, tap the destination to view DestinationDetailView
. From here, tap the Share button in the top-right corner. Select your desired delivery method and send the invitation.