Sharing Core Data With CloudKit in SwiftUI

Learn to share data between CoreData and CloudKit in a SwiftUI app. By Marc Aupont.

4.7 (9) · 5 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. 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.
  2. Creates a UICloudSharingController using the share and container properties. The presentation style is set to .formSheet and delegate is set using the CloudSharingCoordinator. This is responsible for conforming to the UICloudSharingControllerDelegate. 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 how CloudSharingView 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.

Add People in CloudKitSharingController

Note: You can also share invitations using a simulator. Open the data you want to share and tap the Share button. From here, copy link and send it via an email using Safari browser in the simulator.

Accepting Share Invitations

Now that you’ve sent an invitation to your second user, you need to set the app to accept the invitation and add the data into the shared store. Navigate to AppDelegate.swift and you’ll see SceneDelegate is empty. Here, you’ll add the code to accept the share.

The first step is to implement the UIKit scene delegate method windowScene(_:userDidAcceptCloudKitShareWith:). When the user taps the link that was shared earlier and accepts the invitation, the delegate calls this method and launches the app. Add the following method to SceneDelegate:

func windowScene(
  _ windowScene: UIWindowScene,
  userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata
) {
  let shareStore = CoreDataStack.shared.sharedPersistentStore
  let persistentContainer = CoreDataStack.shared.persistentContainer
  persistentContainer.acceptShareInvitations(
    from: [cloudKitShareMetadata], into: shareStore
  ) { _, error in
    if let error = error {
      print("acceptShareInvitation error :\(error)")
    }
  }
}

This code first gets a reference to the sharedPersistentStore that you created in the beginning of this tutorial. New in iOS 15 is acceptShareInvitations(from:into:completion:). persistentContainer calls this method. It accepts the share and adds the necessary metadata into the sharedPersistentStore. That’s it!

Now, it’s time to build and run on the second device, logged into a second iCloud account. If you sent the invitation from a real device, that can be a simulator.

When the app comes up, you’ll notice the shared journal entry doesn’t show up. This is because you haven’t accepted the invitation yet. At this point, if you shared the invitation via text message, open Messages and tap the invitation.

iMessage window showing a shared destination message

When the dialog asks whether you would like to open the invitation, choose Open.

An alert dialog with options to open the destination or not

You now see the shared entry on your second device. Amazing!

Destination added