Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS - Notify today extension for Core Data changes in the main app

I have a NSManagedObject called Event that is shared between the host app and today extension. (In Target Membership, both the main app and the widget are checked).

The host app and widget have the same App Group identifier and both share Data Model(In Target Membership, both the main app and the widget are checked).

When I launch(run) the widget in Xcode, it shows all of the app events (Event) that are already saved in the host app. However, when I add a new event, it appears in the host app but NOT in today-widget. If I relaunch the widget, all the events are shown including the last event that previously was not.

This is the method that fetches events. It is defined in TodayViewController of the widget.

private  func fetchEvents(date: Date) {
    let predicates = NSCompoundPredicate(andPredicateWithSubpredicates: [
        NSPredicate(format: "date = %@",Date().startOfDay as CVarArg),
        NSPredicate(format: "startTime >= %@", Date() as CVarArg)
    ])
    if let ev  = try? TPEvent.fetchAll(predicates: predicates, in: persistentManager.context) {
        events = ev
    }
} 

This event is called in viewWillAppear and widgetPerformUpdate.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    fetchEvents(date: Date())
    self.tableView.reloadData()
}


func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
    self.fetchEvents(date: Date() )
    self.tableView.reloadData()
    completionHandler(NCUpdateResult.newData)
}

persistentManaged.context is PersistentManager.shared.context (see code below).

By the way, both of the methods above are called when I view today-widget. I have a lot of time figuring out this issue but could not do so.

What could be the issue and how to fix it?

Please just comment should you need more info or have any question.

Update

I have a singleton PersistentManager. Use viewContext both in the host app and widget.

 public final class PersistentManager {

    init() {}

    public static let shared = PersistentManager()

    public lazy var persistentContainer: NSPersistentContainer = {

        let container = NSPersistentCloudKitContainer(name: "Event")

        guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.event.data") else {
            fatalError("Shared file container could not be created.")
        }

        let storeURL = fileContainer.appendingPathComponent("Event.sqlite")
        let storeDescription = NSPersistentStoreDescription(url: storeURL)
        container.persistentStoreDescriptions = [storeDescription]

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })

        container.viewContext.automaticallyMergesChangesFromParent = true

        do {
            try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
            fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
        }

        return container
    }()

    public lazy var context = persistentContainer.viewContext

    // MARK: - Core Data Saving support
    public  func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {

                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}
like image 878
mahan Avatar asked Feb 06 '20 21:02

mahan


1 Answers

The issue is that the main app and the app extension work as two different processes on iOS.

CoreData works with NotificationCenter which sends notifications only within the main app process. Thus, you have to send interprocess notification here.

One hidden way to send interprocess notification on iOS is to use KVO on the UserDefaults object.

In NSUserDefaults.h header file Apple states that

/*! NSUserDefaultsDidChangeNotification is posted whenever any user defaults changed within the current process, but is not posted when ubiquitous defaults change, or when an outside process changes defaults. Using key-value observing to register observers for the specific keys of interest will inform you of all updates, regardless of where they're from. */

Having this specified, one can assume that by using KVO on the particular key of UserDefaults, the value change will be propagated from the app to the extension, and vice versa.

So, the approach can be that on each change in the main app you save the current timestamp of the change into the UserDefaults:

/// When the change is made in the main app:
let defaults = UserDefaults(suiteName: "group.<your bundle id>")
defaults["LastChangeTimestamp"] = Date()
defaults.synchronize()

In the app extension:

let defaults = UserDefaults(suiteName: "group.<your bundle id>")

func subscribeForChangesObservation() {
    defaults?.addObserver(self, forKeyPath: "LastChangeTimestamp", options: [.new, .initial], context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    // Process your changes here.
}

deinit {
    defaults?.removeObserver(self, forKeyPath: "LastChangeTimestamp")
}
like image 69
Eugene Dudnyk Avatar answered Oct 20 '22 02:10

Eugene Dudnyk