Accessing Core Data SQL Database in iOS 8 Extension (Sharing Data Between App and Widget Extension)

Accessing Core Data SQL Database in iOS 8 Extension (Sharing Data Between App and Widget Extension)

Widgets are unable to access the NSDocuments directory, which is where one would normally store their database.

The solution is to first create an App Group

Go to:
Project - Target - App Groups - Add New Container

Name the container, i.e. 'group.mycontainer'

Repeat the process for the Widget's Target using the same name for the container.

Then write your database to your group container.

So:

NSURL *storeURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory  inDomains:NSAllDomainsMask] lastObject];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

Becomes:

NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.mycontainer"];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

And initialising the store should be like so:

NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.mycontainer"];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

NSPersistentStore *store = nil;
store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]

Accessing Core Data SQL Database in iOS 8 Extension (Sharing Data Between App and Widget Extension)

Widgets are unable to access the NSDocuments directory, which is where one would normally store their database.

The solution is to first create an App Group

Go to:
Project - Target - App Groups - Add New Container

Name the container, i.e. 'group.mycontainer'

Repeat the process for the Widget's Target using the same name for the container.

Then write your database to your group container.

So:

NSURL *storeURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory  inDomains:NSAllDomainsMask] lastObject];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

Becomes:

NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.mycontainer"];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

And initialising the store should be like so:

NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.mycontainer"];
storeURL = [storeURL URLByAppendingPathComponent:@"db.sqlite"];

NSPersistentStore *store = nil;
store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]

iOS today extension with core data

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!
}
...
}

App Extension Programming Guide on sharing Core Data context with the main app

You can't share viewContext between the app and an extension. I don't mean you shouldn't, I mean it's actually, literally impossible even if you wanted to do so. An app and its extension are two separate processes. The viewContext object is created at run time by a process. There's no way for an app to hand off a variable in memory to a different process on iOS, so there's no possibility of using the same instance in both. You also have two different persistent container objects, again because they're created when the app or extension runs.

These two containers or view contexts might well use the same persistent store file. That's not unusual, and it allows the app and extension to access the same data.

Share Core Data database between app and today extension in Swift

I found an answer here:
https://github.com/pjchavarria/Swift-Widget

This git project shows how to use core data in a widget.

Can an application tune in to Core Data changes made by an extension?

I found a solution to the problem I described after being pointed towards using notification by @CerlinBoss. It is possible to send a notification from the extension to the application (or vice versa). This can be done in iOS using a Darwin notification center. The limitation however is that you can't use the notification to send custom data to your application.

After reading many articles I decided that I'd avoid making changes to the Core Data database from two different processes and using multiple managed contexts. Instead, I queue the data I need to communicate to the application inside a key in the UserDefaults and once the application is notified of the changes, I'd dequeue them and update the Core Data context.

Common Code

Swift 4.1

import os
import Foundation

open class UserDefaultsManager {

// MARK: - Properties

static let applicationGroupName = "group.com.organization.Application"

// MARK: - Alert Queue Functions

public static func queue(notification: [AnyHashable : Any]) {
guard let userDefaults = UserDefaults(suiteName: applicationGroupName) else {
return
}
// Retrieve the already queued notifications.
var alerts = [[AnyHashable : Any]]()
if let data = userDefaults.data(forKey: "Notifications"),
let items = NSKeyedUnarchiver.unarchiveObject(with: data) as? [[AnyHashable : Any]] {
alerts.append(contentsOf: items)
}
// Add the new notification to the queue.
alerts.append(notification)
// Re-archive the new queue.
let data = NSKeyedArchiver.archivedData(withRootObject: alerts)
userDefaults.set(data, forKey: "Notifications")
}

public static func dequeue() -> [[AnyHashable : Any]] {
var notifications = [[AnyHashable : Any]]()
// Retrieve the queued notifications.
if let userDefaults = UserDefaults(suiteName: applicationGroupName),
let data = userDefaults.data(forKey: "Notifications"),
let items = NSKeyedUnarchiver.unarchiveObject(with: data) as? [[AnyHashable : Any]] {
notifications.append(contentsOf: items)
// Remove the dequeued notifications from the archive.
userDefaults.removeObject(forKey: "Notifications")
}
return notifications
}
}

Extension:

Swift 4.1

  override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
os_log("New notification received! [%{public}@]", bestAttemptContent.body)
// Modify the notification content here...
// Queue the notification and notify the application to process it
UserDefaultsManager.queue(notification: bestAttemptContent.userInfo)
notifyApplication()
contentHandler(bestAttemptContent)
}
}

func notifyApplication() {
let name: CFNotificationName = CFNotificationName.init("mutableNotificationReceived" as CFString)
if let center = CFNotificationCenterGetDarwinNotifyCenter() {
CFNotificationCenterPostNotification(center, name, nil, nil, true)
os_log("Application notified!")
}
}

Application:

Swift 4.1

  // Subscribe to the mutableNotificationReceived notifications from the extension.
if let center = CFNotificationCenterGetDarwinNotifyCenter() {
let name = "mutableNotificationReceived" as CFString
let suspensionBehavior = CFNotificationSuspensionBehavior.deliverImmediately
CFNotificationCenterAddObserver(center, nil, mutableNotificationReceivedCallback, name, nil, suspensionBehavior)
}

let mutableNotificationReceivedCallback: CFNotificationCallback = { center, observer, name, object, userInfo in
let notifications = UserDefaultsManager.dequeue()
for notification in notifications {
// Update your Core Data contexts from here...
}
print("Processed \(notifications.count) dequeued notifications.")
}


Related Topics



Leave a reply



Submit