How to deal with Mac OS X Helper/Main app architecture regarding core data, shared preferences and notifications?

I'm having some architectural doubts about a project (Mac OS X app) I'm working on. It basically consists of two elements: a daemon that runs in the background gathering some data and a viewer used to represent the gathered data.

The daemon should be visible in the status bar (no dock icon) and includes a small menu accessible via the status bar. It saves data in a core data store. One of the menu items is a link which opens the viewer. When this viewer is opened, a normal GUI application should start including a dock icon and menubar. The viewer is also opened when opening the application itself (by double-clicking on the icon).

After some experimenting, I figured out the best way to achieve this functionality is by creating two applications, the main application representing the viewer and a helper utility representing the daemon. One of the reasons I did it this way is that it isn't possible to switch between LSUIElement values instantly to force the daemon/viewer state.

Now I have some questions about this architecture:

  • Both the daemon and viewer application uses the same core data store to save and retrieve data. When having a multi-threaded application I know multiple NSManagedObjectContext objects are needed to correctly synchronize data. What about having multiple applications using the same core data store simultaneously? Is this even possible without having the risk of conflicts, locks, etc.? How do I guarantee consistency?

  • The daemon should always start when the viewer starts. I achieved this by simply looping through all open processes and checking if the bundle identifier of the daemon is listed. If not, the daemon is started using NSWorkspace's launchApplication. This works fine. Now when the user quits the daemon, the viewer should also stop. What is the best way for the viewer to be notified of the daemon stopping? I can periodically check active processes and quit the viewer if the daemon is gone but that sounds a bit odd. I would rather choose some kind of notification that I'll send when the viewer is about to close. But since this notification should be sent and captured between apps I don't know which simple notification service is available. Any thoughts?

  • The application is sandboxed as it will be distributed on the Mac App Store. Starting apps with NSWorkspace's launchApplication causes the target app to run in the same sandboxed environment as the source which I think is not a problem at all because running both applications in the same sandbox feels better and probably is. But imagine this scenario: the daemon is started automatically at login (using SMLoginItemSetEnabled) and the user double-clicks Viewer.app. As the daemon is already running (again, this is checked by looping through active processes) it won't be started. Now we have the daemon and the viewer running in different sandboxes right? Will this cause any problems regarding preferences, core data store, etc.?

  • I would like to use NSUserDefaults for basic configuration, can I somehow interchange this data between the daemon and the viewer? Again, both applications will have different bundle identifiers.

Thanks in advance for your help, appreciated!


Solution 1:

There isn't one right answer to this problem, but here's how I'd approach it:

Both the daemon and viewer application uses the same core data store to save and retrieve data.

Because sharing a Core Data store between processes isn't supported (as far as I know), I'd have the daemon expose an XPC Service. Instead of opening the Core Data store itself, the viewer would use an NSXPCConnection to access the data via the daemon.

Assuming the viewer never runs without the daemon, it can use SMLoginItemSetEnabled, like you mentioned in the question, to register a mach service for the daemon, and then connect to that service.

There's sample code that goes over the details of setting that up here on Apple's website (summary: the daemon needs to be at App.app/Contents/Library/LoginItems/daemon.bundle.id.app), and you might also want to read this blog post that discusses some extra requirements imposed by sandboxing (summary: make extra sure that your Team ID is in the daemon's bundle identifier).

The daemon should always start when the viewer starts.

All set: once you register the daemon with SMLoginItemSetEnabled, launchd will start it (if necessary) when the viewer connects to its XPC Service.

Now when the user quits the daemon, the viewer should also stop.

The viewer can use the NSXPCConnection to find out when the daemon quits. The daemon can also use SMLoginItemSetEnabled to un-register itself before it quits, so that it doesn't get relaunched.

I would like to use NSUserDefaults for basic configuration, can I somehow interchange this data between the daemon and the viewer? Again, both applications will have different bundle identifiers.

Use a suite for this:

// To read or write:
NSUserDefaults* suiteDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.example.app.shared"];
[suiteDefaults setObject:[NSDate date] forKey:@"launchTime"];

// Add the suite to -standardUserDefaults to make reading easier:
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"com.example.app.shared"];

To work with sandboxing, the viewer and the daemon must share an App Group. You can even use KVO to observe changes to shared keys.