Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2021 SwiftUI & UI FrameworksSystem Services

WWDC21 · 25 min · SwiftUI & UI Frameworks / System Services

Build apps that share data through CloudKit and Core Data

Learn how to easily build apps that share data between multiple iCloud users with NSPersistentCloudKitContainer. Discover how to create informative experiences around shared data and learn about the CloudKit technologies that support these features in Core Data. To get the most out of this session, check out our previous videos on NSPersistentCloudKitContainer: "Using Core Data With CloudKit" from WWDC19 and "Sync a Core Data store with the CloudKit public database" from WWDC20.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 6 snippets

Add shared store description swift · at 5:20 ↗
let privateStoreDescription = container.persistentStoreDescriptions.first!
let storesURL = privateStoreDescription.url!.deletingLastPathComponent()
privateStoreDescription.url = storesURL.appendingPathComponent("private.sqlite")
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

let sharedStoreURL = storesURL.appendingPathComponent("shared.sqlite")
let sharedStoreDescription = privateStoreDescription.copy()
sharedStoreDescription.url = sharedStoreURL

let containerIdentifier = privateStoreDescription.cloudKitContainerOptions!.containerIdentifier
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
container.persistentStoreDescriptions.append(sharedStoreDescription)
shareNoteAction, DetailViewController.swift swift · at 6:00 ↗
@IBAction func shareNoteAction(_ sender: Any) {
  guard let barButtonItem = sender as? UIBarButtonItem else {
    fatalError("Not a UI Bar Button item??")
  }

  guard let post = self.post else {
    fatalError("Can't share without a post")
  }

  let container = AppDelegate.sharedAppDelegate.coreDataStack.persistentContainer
  let cloudSharingController = UICloudSharingController {
    (controller, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) in
    container.share([post], to: nil) { objectIDs, share, container, error in
			if let actualShare = share {
				post.managedObjectContext?.performAndWait {
					actualShare[CKShare.SystemFieldKey.title] = post.title
				}
			}
			completion(share, container, error)
		}
  }
  cloudSharingController.delegate = self

  if let popover = cloudSharingController.popoverPresentationController {
    popover.barButtonItem = barButtonItem
  }
  present(cloudSharingController, animated: true) {}
}
SharingProvider swift · at 17:06 ↗
protocol SharingProvider {
  func isShared(object: NSManagedObject) -> Bool
  func isShared(objectID: NSManagedObjectID) -> Bool
  func participants(for object: NSManagedObject) -> [RenderableShareParticipant]
  func shares(matching objectIDs: [NSManagedObjectID]) throws -> [NSManagedObjectID: RenderableShare]
  func canEdit(object: NSManagedObject) -> Bool
  func canDelete(object: NSManagedObject) -> Bool
}
Decorate table cells for shared posts, MainViewController.swift swift · at 17:58 ↗
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath) as? PostCell else {
        fatalError("###\(#function): Failed to dequeue a PostCell. Check the cell reusable identifier in Main.storyboard.")
    }
    let post = dataProvider.fetchedResultsController.object(at: indexPath)
    cell.title.text = post.title
    cell.post = post
    cell.collectionView.reloadData()
    cell.collectionView.invalidateIntrinsicContentSize()
    
    if let attachments = post.attachments, attachments.allObjects.isEmpty {
        cell.hasAttachmentLabel.isHidden = true
    } else {
        cell.hasAttachmentLabel.isHidden = false
    }
    
    if sharingProvider.isShared(object: post) {
        let attachment = NSTextAttachment(image: UIImage(systemName: "person.circle")!)
        let attributedString = NSMutableAttributedString(attachment: attachment)
        attributedString.append(NSAttributedString(string: " " + (post.title ?? "")))
        cell.title.text = nil
        cell.title.attributedText = attributedString
    }
    return cell
}
Testing the MainViewController's table view, TestMainViewController.swift swift · at 18:44 ↗
func testSharedPostsGetDisclosure() {
    var sharedObjectIDs: Set<NSManagedObjectID> = Set()
    let context = coreDataStack.persistentContainer.viewContext
    self.generatePosts(in: context, postSaveBlock: { posts in
        for (index, post) in posts.enumerated() where (index % 4) == 0 {
            sharedObjectIDs.insert(post.objectID)
        }
    })
    
    let provider = BlockBasedShareProvider(stack: coreDataStack)
    provider.isSharedBlock = sharedObjectIDs.contains
    mainViewController.sharingProvider = provider
    
    do {
        try mainViewController.dataProvider.fetchedResultsController.performFetch()
    } catch let error {
        XCTFail("Error while fetching \(error)")
    }
    
    reloadTableView()
    let rowCount = mainViewController.tableView(mainViewController.tableView,
                                                    numberOfRowsInSection: 0)
    XCTAssertEqual(100, rowCount)
    guard let expectedSharedImage = UIImage(systemName: "person.circle") else {
        XCTFail("Failed to get the person system image.")
        return
    }
    
    for index in 0..<rowCount {
        let indexPath = IndexPath(row: index, section: 0)
        let post = mainViewController.dataProvider.fetchedResultsController.object(at: indexPath)
        guard let title = post.title else {
            XCTFail("All posts should have been given a title.")
            return
        }
        
        guard let cell = mainViewController.tableView(mainViewController.tableView,
                                                           cellForRowAt: indexPath) as? PostCell else {
            XCTFail("Encountered an unexpected cell type in the main view controller's table view.")
            return
        }
        
        if sharedObjectIDs.contains(post.objectID) {
            guard let attributedText = cell.title.attributedText else {
                XCTFail("Failed to get the attributed text of \(cell). Was it not set?")
                return
            }
            
            guard let attachment = attributedText.attributes(at: 0, effectiveRange: nil)[.attachment] as? NSTextAttachment else {
                XCTFail("Expected an image attachment at the first character.")
                return
            }
            
            XCTAssertEqual(expectedSharedImage, attachment.image)
        } else {
            XCTAssertEqual(cell.title.text, title)
        }
    }
}

class BlockBasedShareProvider: SharingProvider {
    var coreDataStack: CoreDataStack
    init(stack: CoreDataStack) {
        coreDataStack = stack
    }
    
    func isShared(object: NSManagedObject) -> Bool {
        return isShared(objectID: object.objectID)
    }
    
    public var isSharedBlock: ((_ object: NSManagedObjectID) -> Bool)? = nil
    func isShared(objectID: NSManagedObjectID) -> Bool {
        guard let block = isSharedBlock else {
            return coreDataStack.isShared(objectID: objectID)
        }
        return block(objectID)
    }
    
    public var participantsBlock: ((_ object: NSManagedObject) -> [RenderableShareParticipant])? = nil
    func participants(for object: NSManagedObject) -> [RenderableShareParticipant] {
        guard let block = participantsBlock else {
            return coreDataStack.participants(for: object)
        }
        return block(object)
    }
    
    public var sharesBlock: ((_ objectIDs: [NSManagedObjectID]) -> [NSManagedObjectID: RenderableShare])? = nil
    func shares(matching objectIDs: [NSManagedObjectID]) throws -> [NSManagedObjectID: RenderableShare] {
        guard let block = sharesBlock else {
            return try coreDataStack.shares(matching: objectIDs)
        }
        return block(objectIDs)
    }
    
    public var canEditBlock: ((_ object: NSManagedObject) -> Bool)? = nil
    func canEdit(object: NSManagedObject) -> Bool {
        guard let block = canEditBlock else {
            return coreDataStack.canEdit(object: object)
        }
        return block(object)
    }
    
    public var canDeleteBlock: ((_ object: NSManagedObject) -> Bool)? = nil
    func canDelete(object: NSManagedObject) -> Bool {
        guard let block = canDeleteBlock else {
            return coreDataStack.canDelete(object: object)
        }
        return block(object)
    }
}
CoreDataStack + Sharing, CoreDataStack.swift swift · at 20:01 ↗
extension CoreDataStack: SharingProvider {
    func isShared(object: NSManagedObject) -> Bool {
        return isShared(objectID: object.objectID)
    }

    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 nil != shares.first {
                        isShared = true
                    }
                } catch let error {
                    print("Failed to fetch share for \(objectID): \(error)")
                }
            }
        }
        return isShared
    }
    
    func participants(for object: NSManagedObject) -> [RenderableShareParticipant] {
        var participants = [CKShare.Participant]()
        do {
            let container = persistentContainer
            let shares = try container.fetchShares(matching: [object.objectID])
            if let share = shares[object.objectID] {
                participants = share.participants
            }
        } catch let error {
            print("Failed to fetch share for \(object): \(error)")
        }
        return participants
    }
    
    func shares(matching objectIDs: [NSManagedObjectID]) throws -> [NSManagedObjectID: RenderableShare] {
        return try persistentContainer.fetchShares(matching: objectIDs)
    }
    
    func canEdit(object: NSManagedObject) -> Bool {
        return persistentContainer.canUpdateRecord(forManagedObjectWith: object.objectID)
    }
        
    func canDelete(object: NSManagedObject) -> Bool {
        return persistentContainer.canDeleteRecord(forManagedObjectWith: object.objectID)
    }
}

Resources