6

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 :

  • enabled appGroups for both ios app and today extension targets.
  • coded a function to share it :
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/

  • were you able to figure out how to connect your data to your today extension? I'm currently having the same issue – fphelp Apr 21 '20 at 04:02
  • @fphelpare you using a cloudKit container ? – Cydiaddict Apr 22 '20 at 04:00
  • Yes I use CloudKit container when the user has turned iCloud on and a regular NSPersistentContainer when it's off – fphelp Apr 23 '20 at 17:07
  • you can use the cloud container for both, try to add yourStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "yourContainerID") – Cydiaddict Apr 24 '20 at 18:05

2 Answers2

4

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!
    }
    ...
}
hidden-username
  • 2,610
  • 3
  • 14
  • 19
  • I get this error on the `defaultDirectoryURL()` function line: `Overriding non-@objc declarations from extensions is not supported`. I used exactly what you wrote so I am unsure why this is happening – fphelp Apr 21 '20 at 04:14
  • @fphelp I haven't seen that error, but it sounds like you are declaring this class in your extension. Declare it in your App, and just make sure it accessible in your extension by setting the target to both. – hidden-username Apr 22 '20 at 21:24
  • I took a break from the widget as it was too hard, but now I'm back to it and had to recode everything. I just added the GroupedPersistentCloudKitContainer to my code, but when I try to run the app I get an error at the line `if !FileManager.default.fileExists(atPath: url!.path) {` saying that it can not error unwrapping url. I believe this means the url is nil. I triple checked and I am using the correct string for `URLStrings.group.rawValue`. Any guess why this is happening? – fphelp May 17 '20 at 00:53
  • I added a new question here so it's easier and you can get points: https://stackoverflow.com/questions/61845865/app-groups-forsecurityapplicationgroupidentifier-returns-nil – fphelp May 17 '20 at 01:52
1

The url for your app group is here:

let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "SchoolCompanion)

The sqlite-file is there. You don't need Cloud container.

Pavel
  • 421
  • 1
  • 4
  • 14
  • my container is a NSPersistentCloudKit container, that mean it's synced to iCloud. – user10768342 Mar 02 '20 at 14:44
  • in the link in my post it's not like that. Here you are passing the container name as argument but that should be the appGroup identifier like as described – user10768342 Mar 02 '20 at 14:46