iosswiftxcodecore-datatoday-extension

iOS today extension with core data


I am trying to make a today extension for my ios app. That today extension will display the 'next' Course based on data saved with core data. I've been doing some research and I understand I have to share my persistentContainer with an appGroup. So I did :

public extension NSPersistentContainer {
    func addToAppGroup(id: String) {
        guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id) else {
            fatalError("Shared file container could not be created.")
        }
        let storeURL = fileContainer.appendingPathComponent("\(self.name).sqlite")
        let storeDescription = NSPersistentStoreDescription(url: storeURL)
        self.persistentStoreDescriptions.append(storeDescription)
    }
}

then in my core data stack :

internal class CoreDataContainer {

    static var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentCloudKitContainer(name: "SchoolCompanion")
        container.addToAppGroup(id: "group.com.Ce-dricLoneux.School-Companion")
        container.loadPersistentStores(completionHandler: { (_, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = CoreDataContainer.persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

that files and the xcdatamodel are shared between both targets. At this point I thought I could access my core data from my extension but when i do a fetch request I don't get any result. The controllerDidChangeContent is never executed.

class TodayViewController: UIViewController, NCWidgetProviding {
private func configureFetchedResultsController() {
        let request: NSFetchRequest<Course> = Course.fetchRequest()
        request.sortDescriptors =  []
        self.fetchedResultsController = NSFetchedResultsController<Course>(fetchRequest: request, managedObjectContext: CoreDataContainer.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        self.fetchedResultsController.delegate = self
        do {
            try self.fetchedResultsController.performFetch()
        } catch {
            print(error.localizedDescription)
        }
    }

override func viewDidLoad() {
        super.viewDidLoad()

        self.extensionContext?.widgetLargestAvailableDisplayMode = .compact
        self.configureFetchedResultsController()

    }

func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
        // Perform any setup necessary in order to update the view.

        // If an error is encountered, use NCUpdateResult.Failed
        // If there's no update required, use NCUpdateResult.NoData
        // If there's an update, use NCUpdateResult.NewData

        completionHandler(NCUpdateResult.newData)
    }
}

// MARK: - FetchedResultsController Delegate

extension TodayViewController: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        print("content did change")
    }
}

I tried to make a fetch request direclty too, buti get an empty array as result. Why does I don't get any result ?

Another thing : i used nsfetchedResult controller in order to refresh the view data each time the data is updated from the ios app so is that ok or should I use the widgetPerformUpdate method ? In that case we don't know when the today extension will be refreshed and it may dispay outdated data.

main documentation used : https://www.avanderlee.com/swift/core-data-app-extension-data-sharing/


Solution

  • You should subclass NSPersistentCloudKitContainer like below, returning the App Group URL for defaultDirectoryURL(). Then in your CoreDataStack, use let container = GroupedPersistentCloudKitContainer(name: "SchoolCompanion"). Also remove, your call to addToAppGroup(...). You will need to instantiate the GroupedPersistentCloudKitContainer in both the App and the Extension, you will also need to make sure the GroupedPersistentCloudKitContainer is linked to both Targets.

    class GroupedPersistentCloudKitContainer: NSPersistentCloudKitContainer {
    
        enum URLStrings: String {
            case group = "group.com.yourCompany.yourApp"
        }
    
    
        override class func defaultDirectoryURL() -> URL {
            let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: URLStrings.group.rawValue)
    
            if !FileManager.default.fileExists(atPath: url!.path) {
                try? FileManager.default.createDirectory(at: url!, withIntermediateDirectories: true, attributes: nil)
            }
            return url!
        }
        ...
    }