iOS: Writing to Core Data in your Today extension

Carmen Burmeister

A Today extension, also known as a widget, is a great way to present up-to-date information in the Notification Center. This information might be directly fetched from the web, but can also be related to a local feature of your app and show its status.

If you have experience with widgets, you have probably used Core Data to share the data between your app and its widget. Accessing the app’s Core Data store from inside the widget is not a big deal when both the app and the widget are members of the same app group and the database file is stored in the shared container. In case your widget just reads data and doesn’t write to the store, you won’t get any trouble with synchronization. The fetched objects of your app’s managed object context will always represent what is currently on disk.

Today extensions can do more than just show content. They can also include controls, e.g. buttons, to offer some simple interaction. But in this case it might be necessary to manipulate, add or delete entries in the database. For the managed objects fetched by the app this means they become outdated. No notification like NSManagedObjectContextDidSaveNotification is sent because the app and the widget don’t run in the same process.

One way to solve this could be to let the widget set a flag in the user defaults shared with the app. The app can react on the flag when it becomes active by resetting the managed object context. Unfortunately, in most cases this is not practicable. This approach requires to discard references to all the fetched managed objects since they are invalid afterwards.

The ideal solution, then, is to apply the specific set of changes to the managed object context to bring it in line with what is on disk. The good news: Core Data already supports a situation where the database file was changed by another process. This is how changes from iCloud are being adopted. It uses the method mergeChangesFromRemoteContextSave of NSManagedObjectContext which handles the merge based on the provided dictionary of changes. The keys must be those known from the NSManagedObjectContextDidSaveNotification. The values should be an array of NSManagedObjectID objects or NSURL objects containing a URI.

The trick now is to take those sets of managed objects delivered by NSManagedObjectContextDidSaveNotification in the widget, produce exactly the required notification data needed for mergeChangesFromRemoteContextSave and propagate it via the shared NSUserDefaults. Since NSManagedObjectID doesn’t implement NSCoding and therefore can’t be serialized, we use its URI which provides an archivable reference. The following method shows how this can be done:

func handleContextDidSave(notification: NSNotification) {
    guard let userInfo = notification.userInfo else {
        return
    }

    let userDefaults = NSUserDefaults(suiteName: "group.com.yourdomain.YourGroup")

    var notificationData = [NSObject : AnyObject]()

    for (key, value) in userInfo {
        guard let value = value as? NSSet else {
            continue
        }

        var uriRepresentations = [AnyObject]()

        for element in value {
            guard let managedObject = element as? NSManagedObject else {
                continue
            }

            uriRepresentations.append(managedObject.objectID.URIRepresentation())
        }

        notificationData[key] = uriRepresentations
    }

    var notificationDatas = [AnyObject]()

    if let data = userDefaults!.objectForKey(TRWidgetObjectChangesKey) as? NSData,
       let existingNotificationDatas = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [AnyObject] {
        notificationDatas.appendContentsOf(existingNotificationDatas)
    }

    notificationDatas.append(notificationData)

    let archivedData: NSData = NSKeyedArchiver.archivedDataWithRootObject(notificationDatas)

    userDefaults!.setObject(archivedData, forKey: "anKey")

    userDefaults!.synchronize()
}

Your app can now unarchive the data as soon as it becomes active. This can be done either directly in applicationDidBecomeActive or by listening to the UIApplicationDidBecomeActiveNotification notification. The notifications caught and archived in the widget are being merged in the same order they occured:

func mergeChangesFromWidget() {
    let userDefaults = NSUserDefaults(suiteName: "group.com.yourdomain.YourGroup")

    defer {
        userDefaults!.removeObjectForKey("anKey")

        userDefaults!.synchronize()
    }

    guard let data = userDefaults!.objectForKey("anKey") as? NSData else {
        return
    }

    guard let notificationsArray = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [AnyObject] else {
        return
    }

    self.managedObjectContext.performBlock {
        for notificationData in notificationsArray {
            guard let notificationData = notificationData as? [NSObject : AnyObject] else {
                continue
            }

            NSManagedObjectContext.mergeChangesFromRemoteContextSave(notificationData, intoContexts: [self.managedObjectContext])
        }
    }
}

The effect you are getting is the same as when calling mergeChangesFromContextDidSaveNotification after a NSManagedObjectContextDidSaveNotification notification. The changes will be picked up by the fetched results controllers and by any other object registered on notifications from the context. The UI can then be updated properly avoiding a full reload.

Thumb publicpreview 2

Carmen is a consultant at innoQ Germany with more than ten years of experience in mobile application development. Her focus and passion is to create apps for iOS devices.

More content

Comments

Please accept our cookie agreement to see full comments functionality. Read more