Search code examples
iosswiftwatchos

Why can't my watch app see data from my iOS Core Data app?


I've been following along with a Hacking with Swift tutorial where he makes an app that syncs data across iPhone, iPad and Mac versions of his app. All the syncing is handled by Core Data and Cloud Kit.

I've been trying to adapt what he did to my own app which has a companion watchOS app. I'm using NSPersistentCloudKitContainer

I've put the iOS app and the watchOS app in the same app group and I'm loading the shared app container with the correct app group identifier (my widget can see the data just fine), but the watch app can never see the data. The watch app doesn't crash, it just doesn't see anything to load, I get back an empty array of entities.

Can watch apps not use this method? Am I missing some key step?

Here's the DataController class:

import CoreData
import SwiftUI

class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
private var saveTask: Task<Void, Error>?

static var preview: DataController = {
    let dataController = DataController(inMemory: true)
    return dataController
}()

private var sharedStoreURL: URL {
    let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.MyCo.MyApp1")!
    return container.appendingPathComponent("Model.sqlite")
}



static let model: NSManagedObjectModel = {
    guard let url = Bundle.main.url(forResource: "Model", withExtension: "momd") else {
        fatalError("Failed to locate model file.")
    }

    guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
        fatalError("Failed to load model file.")
    }

    return managedObjectModel
}()


init(inMemory: Bool = false) {
    container = NSPersistentCloudKitContainer(name: "Model", managedObjectModel: Self.model)
    
    if inMemory {
        container.persistentStoreDescriptions.first?.url = URL(filePath: "/dev/null")
    } else {
        container.persistentStoreDescriptions.first?.url = sharedStoreURL
    }
    
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    
    container.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator, queue: .main, using: remoteStoreChanged)
    
    container.loadPersistentStores { _, error in
        if let error {
            fatalError("Fatal error loading store: \(error.localizedDescription)")
        }
        
    }
}



func remoteStoreChanged(_ notification: Notification) {
    objectWillChange.send()
}


func save() {
    saveTask?.cancel()
    
    if container.viewContext.hasChanges {
        try? container.viewContext.save()
    }
}


func queueSave() {
    saveTask?.cancel()
    
    saveTask = Task { @MainActor in
        try await Task.sleep(for: .seconds(3))
        save()
    }
}


func delete(_ object: NSManagedObject) {
    objectWillChange.send()
    container.viewContext.delete(object)
    save()
}



private func delete(_ fetchRequest: NSFetchRequest<NSFetchRequestResult>) {
    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    batchDeleteRequest.resultType = .resultTypeObjectIDs
    
    if let delete = try? container.viewContext.execute(batchDeleteRequest) as? NSBatchDeleteResult {
        let changes = [NSDeletedObjectsKey: delete.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [container.viewContext])
    }
}


func getMyEntities() throws -> [MyEntity] {
    let request: NSFetchRequest<MyEntity> = MyEntity.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(keyPath: \MyEntity.name, ascending: true)]
   
    let entities = try container.viewContext.fetch(request)
   
    return entities
}

}

EDIT -- The critical piece I was missing was adding two capabilities to each of the iOS app target and the watchOS app target.

First, I needed to add the iCloud capability with the CloudKit service selected and create a container to be shared between the iOS app and the watchOS app.

Second, I needed to add the Background Modes capability and select the Background fetch mode (for iOS only) and Remote notifications mode (iOS and watchOS).


Solution

  • The AppGroup container is for Apps within the same device. That is why it works between the your iOS App and the Widget.

    AppGroups use "interprocess communication (IPC)" which happens in shared memory on the device. AppGroups do not support iCloud or any other synching process that would allow WatchOS to connect.

    If you want to share data between iOS and WatchOS you can use CloudKit and share the same iCloud Container between the devices or use WatchConnectivity to send the new data to WatchOS.

    The Hacking With Swift videos likely use the same CloudKit Container, it is the simplest option. Just make sure that all the targets match under "Signing and Capabilities"

    Here are some helpful links.

    https://developer.apple.com/wwdc21/10003

    https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/setting_up_core_data_with_cloudkit

    https://developer.apple.com/documentation/watchconnectivity/implementing_two-way_communication_using_watch_connectivity