ylliX - Online Advertising Network

Sharing data between iOS apps and app extensions


Since iOS app extensions run as part of a host application rather than as part of their containing app (i.e. your app’s extensions run in somebody else’s app), data sharing isn’t automatic. Finding standard locations like the documents directory doesn’t work for shared data. In this post I’ll go through the details of how to make it all work.

Along the way I’ll get into how to set up real-time messaging between apps and their extensions. Not Cocoa notifications, but a variation of file-based IPC that includes a notification system.

Most of this is not actually specific to iOS extensions, though it’s probably more useful with extensions than in other situations.

Code snippets are included, and see GitHub for the full project.

Sharing non-local or non-app data

Of course the easy way is to just not bother sharing local app-specific data at all. If the data is either non-local or not specific to your app, sharing may already be covered.

Non-local data implies that it’s linked to or available from some out-of-app source, like a web server somewhere. Since app extensions often don’t run for very long, adding network latency might not be viable. But in principle there’s no reason an extension can’t make the same network calls as the app.

Apple’s approach to this in their Lister demo app is to use iCloud with Core Data. That’s also non-local since it syncs to the iCloud service, but has the benefit of system-level local caching to avoid network delays. Of course, Core Data with iCloud has its own set of issues…

Data that’s not specific to your app would be something like the iOS address book database. If you’re using data where Apple already gives you an API for shared data, you’re set.

Set up an App Group

App Groups are the scheme iOS uses to allow different apps to share data. If the apps have the right entitlements and proper provisioning, they can access a shared directory outside of their normal iOS sandbox. Sandboxing still applies except for a single exception.

This is probably almost automatic, but when you get into iOS app provisioning you can’t assume anything. What’s supposed to happen is that you just turn on the “app groups” entitlement in Xcode for the app and for any extensions.

When you flip that switch, Xcode will talk to the developer center to configure your app ID for app groups. Next it’ll ask you for a group name. Give it one and it’ll create and download a new provisioning profile.

If that doesn’t work (and let’s face it, with provisioning it’s a crapshoot) you can keep trying or else log in to the dev center and do it by hand. It’s less convenient but hardly impossible.

Using your App Group

The simplest way to use the app group is for shared user defaults. It’s extremely easy. Instead of using the ubiquitous [NSUserDefaults standardUserDefaults] call, create a custom user defaults object:

NSUserDefaults *myDefaults = [[NSUserDefaults alloc]
    initWithSuiteName:@"group.com.atomicbird.demonotes"];
[myDefaults setObject:@"foo" forKey:@"bar"];

That’s what Apple describes in the App Extension Programming Guide, and it’s fantastic if you don’t need to share very much data and you don’t need notifications of changes.

If you need to share more data than really works for user defaults, you can access the shared group directory directly via NSFileManager:

NSURL *groupURL = [[NSFileManager defaultManager]
    containerURLForSecurityApplicationGroupIdentifier:
        @"group.com.atomicbird.demonotes"];

Any app or extension with matching group entitlements can access the same directory, so any data saved there is shared among all of them. If you want any sub-directories, you’ll need to create them.

Keep Your Data Intact

But first, make sure that you don’t accidentally corrupt the data. Sharing data files means there might be more than one process trying to use a file at the same time. Sandboxing on iOS means this is a somewhat rare situation, but that doesn’t mean it’s OK to ignore it.

Update, 2015-05-18: The original version of this post recommended using NSFileCoordinator and NSFilePresenter. I later (2014-11-29) removed that because Apple’s Tech Note 2408, warned against doing so, noting that

When you create a shared container for use by an app extension and its containing app in iOS 8, you are obliged to write to that container in a coordinated manner to avoid data corruption. However, you must not use file coordination APIs directly for this.

However last week this was updated to read:

When you create a shared container for use by an app extension and its containing app in iOS 8.0 or later, you are obliged to write to that container in a coordinated manner to avoid data corruption. However, you must not use file coordination APIs directly for this in iOS 8.1.x and earlier. [emphasis mine]

It also notes that problems with file coordination have been resolved in iOS 8.2. As a result I’m restoring the original discussion of data sharing. But note that this only applies with iOS 8.2 and up. Thanks to Ben Chatelain for pointing out this latest change. Also thanks to @lazerwalker and Ari Weinstein for pointing out the original problems.

You’ll want to use NSFileCoordinator any time you want to read or write your shared files. You’ll also want to implement NSFilePresenter any time you need to know if a file has changed. These were introduced as companions to iCloud, where both your app and the iCloud daemon might want to access the same file. They’re not iCloud specific, though.

NSFileCoordinator

NSFileCoordinator implements a read/write lock for file access that can coordinate access between different processes. It helps ensure that a process gets exclusive access to a file when writing to it.

Doing a coordinated read looks something like this, in my demo app:

NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] 
    initWithFilePresenter:self];
NSError *fileCoordinatorError = nil;
__block NSArray *savedNotes = nil;

[fileCoordinator coordinateReadingItemAtURL:[self demoNoteFileURL] options:0 
    error:&fileCoordinatorError byAccessor:^(NSURL *newURL) {
    
    NSData *savedData = [NSData dataWithContentsOfURL:newURL];
    
    if (savedData != nil) {
        savedNotes = [NSKeyedUnarchiver unarchiveObjectWithData:savedData];
    }
}];

The block is where the actual reading takes place, and the rest of the code ensures that the reading doesn’t happen while someone else is changing the file. The self argument to initWithFilePresenter: isn’t mandatory, but if you use NSFilePresenter you should include a presenting object.

A coordinated write follows the same pattern, though the locking happens differently. There are other useful methods on NSFileCoordinator for cases like reading a file, making a change, and then writing the new version.

It’s important to be aware that NSFileCoordinator methods run synchronously, so your code will block until they complete. That’s convenient since you don’t have to wait for an asynchronous block callback. But it also means that they block the current thread. If some other process is going to be busy with the file for a long time, you’ll end up waiting on it.

NSFilePresenter

NSFilePresenter is a protocol that you can add to any class. A file presenter is just any object that cares about the state of the file and wants to know when things happen to it. Most of the methods are optional and are there to notify you that the file has changed in one way or another so that your code can respond. Some other methods advise your code of things it probably should do– for example “hey, now would be a good time to save any changes you have” (savePresentedItemChangesWithCompletionHandler:).

Which file presenter methods you implement depends on how much you need to know about changes to your shared files. The simplest case is probably to use presentedItemDidChange but no others. That’s a generic call that tells you that some other process (your app or your extension) changed the contents of the file. What you do depends on how you use the data.

Make sure to pass the file presenter object to the NSFileCoordinator when you create it. Although it’s not strictly necessary, it helps prevent your code being notified of its own changes. Also, if you’re implementing methods like presentedItemDidChange:, make sure to tell NSFileCoordinator that you’re interested:

[NSFileCoordinator addFilePresenter:self];

Then you should be all set to be notified of changes to the file.

Other options

If you’re not using custom code to read and write your data, you can skip file coordination and instead go with atomic read/write operations. For example, writeToFile:atomically: on property list classes like NSArray should be safe without doing your own file coordination. Likewise any data saved with NSUserDefaults should avoid corruption without extra coordination code. Also, SQLite (and by extension, Core Data) is smart enough to handle having multiple processes access its data.

But this just handles keeping the data intact. You’ll still want to make sure that your app and its extensions are aware of new changes so they can present current data to the user. For that you’ll need some kind of notification system.

Notifications between Apps and App Extensions

There’s still no full IPC mechanism on iOS. NSDistributedNotificationCenter hasn’t made the jump from OS X to iOS and probably never will. But file coordination and presentation can serve the same purpose, as long as the apps use the same app group.

Update, 2015-05-18: As with the discussion above, this section originally used NSFilePresenter, but that was removed, and is now being restored.

When I was adding file coordination and presentation to my demo app, I realized that they could also be used for notifications between an app and its extensions. If one of them does a coordinated write while the other is using a file presenter for the file, the call to presentedItemDidChange happens almost instantly. Notification is the whole purpose of that method, so it makes sense it would work this way. I want to be notified if a specific file changes, and that’s how I get the notification.

But you don’t need to care about the file contents to be interested in notifications. If you just want a notification, choose a file name and use it as the notification mechanism. Any time one process needs to notify the other, make a change to the file. The other will get a file presenter call, and the notification is complete. It feels sort of like a hack but really this is exactly how the API is designed to work.

Another approach that has become very popular is the MMWormhole project. It bypasses file presenters and uses Darwin-level notifications via CFNotificationCenterGetDarwinNotifyCenter. This actually is a little bit like NSDistributedNotificationCenter. Either approach works.

For Watch Apps Only

If you’re writing an Apple Watch app, you have one more option not available to other types of app extension. In your WKInterfaceController subclass, call openParentApplication:reply: to pass data to the containing app and get a response. That will trigger a call to application:handleWatchKitExtensionRequest:reply: in the containing app’s app delegate. This method serves as a live notification but can also carry arbitrary data.

Unlike other approaches, this has the benefit that it will launch the containing app if it’s not already running. Using file coordination or MMWormhole is great but they can’t launch the containing app.

The drawback to this approach is that it can only be initiated from the Watch app. The containing app doesn’t have a corresponding call to tell the Watch app that new data is available.

This scheme only exists for Watch apps, not for other types of app extension. I’m hopeful that Apple will add something comparable (rdar://19207935). For now, other extension types will need to use other communication approaches.





Source link

Leave a Reply

Your email address will not be published. Required fields are marked *