Finder Sync Extension

No Comments

The macOS Finder Sync Extension allows extending the Finder’s UI to show the file synchronization status.
Apple’s documentation on the macOS FinderSyncExtension is good, but it lacks more info on communication between the MainApp and the Finder Sync Extension. In this article, one such example is shown as a possible solution for bidirectional communication between the two.

Creating the extension

Once you’ve created your macOS project, select the project in the project navigator and add the new target “Finder Sync Extension”.

Xcode macOS extensions

Sandboxing

Depending on your project’s needs, you may want/need to disable the App Sandbox. If your project is going to use a virtual file system such as osxfuse, the App Sandbox needs to be disabled.

Xcode Project App Sandbox Settings

You’ll have to leave the App Sandbox enabled for the Finder Sync Extension though. Set ‘User Selected File’ permissions to None in the Finder Sync Extension. If you leave it to ‘Read Only’, which it is by default, the extension won’t be able to receive messages from the MainApp.

Xcode Project App Sandbox Settings Enabled

App groups

To be able to communicate with one another, the MainApp and the Finder Sync Extension must belong to the same App Group. Create an App Group in a pattern like: group.[app-bundle-id]

What the demo app demonstrates

The demo application consists of two components: FinderSyncExample and FinderSyncExtension. FinderSyncExample is what we call the ‘MainApp’.
When started, the MainApp offers a path to a demo folder which will be created when the Set button is pressed. After successful folder creation, the app shows controls for modifying file sync statuses. Beneath the controls, there is a label showing a custom message which can be sent from the Finder extension menu.

MainApp updating file statuses in the Finder

Demo app main window

It is possible to set a status for three files: file1.txt, file2.txt and file3.txt. Select a desired status from combo-box and tap the appropriate Set button. Observe how Finder applies sync status to the relevant file.

Finder with file sync statuses

Finder sending message to the MainApp

On the Finder window, open the SyncExtension menu and select ‘Example Menu Item’ on it. Observe how on the MainApp window message-label is updated to show a message received from the Finder.

Multiple FinderSyncExtension instances can exist simultaneously

Single App can have multiple FSEs running

It is possible that more than one Finder Sync Extension is running. One Finder Sync Extension can be running in a regular Finder window. The other FinderSyncExtension process can be running in an open-file or save-document dialog. In that case, MainApp has to be able to update all FinderSyncExtension instances.
Keep this in mind when designing the communication between the MainApp and the FinderSyncExtension.

Bidirectional communication

Communication between the MainApp and the FinderSyncExtension can be implemented in several ways. The concept described in this article relies on Distributed Notifications. Other options may include mach_ports, CFMessagePort or XPC.
We chose Distributed Notifications because it fits with the One-application – Multiple-extensions concept.

Both the MainApp and the FinderSyncExtension processes are able to subscribe to certain messages. Delivering and receiving messages is like using the well-known NSNotificationCenter.

Sending a message from MainApp to FinderSyncExtension

To be able to receive notifications, FinderSyncExtension registers as an observer for certain notifications:

NSString* observedObject = self.mainAppBundleID;
NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter];
 
[center addObserver:self selector:@selector(observingPathSet:)
               name:@"ObservingPathSetNotification" object:observedObject];
 
[center addObserver:self selector:@selector(filesStatusUpdated:)
               name:@"FilesStatusUpdatedNotification" object:observedObject];

The relevant code is available in the FinderCommChannel class.

For the MainApp to be able to send a message to FinderSyncExtension, use NSDistributedNotificationCenter:

NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter];
[center postNotificationName:name
                      object:NSBundle.mainBundle.bundleIdentifier
                    userInfo:data
          deliverImmediately:YES];

More details are available in the AppCommChannel class.
AppCommChannel belongs to the MainApp target. It handles sending messages to FinderSyncExtension and receiving messages from the extension.
FinderCommChannel belongs to FinderSyncExtension target. It handles sending messages to the MainApp and receiving messages from the MainApp.

Throttling messages

In real-world apps, it can happen that an app wants to update the sync status of many files in a short time interval. For that reason, it may be a good idea to gather such updates and send them all in one notification. macOS will complain about sending too many notifications in a short interval. It can also give up on delivery of notifications in such cases.
The AppCommChannel class shows the usage of NSTimer for throttling support. A timer checks every 250ms if there are queued updates to be delivered to FinderSyncExtension.

For a clearer display, a sequence diagram showing sending messages from the MainApp to FinderSyncExtension is given bellow.

FSE sequence diagram

Sending messages from FinderSyncExtension to MainApp

To send a message from FinderSync to the MainApp, NSDistributedNotificationCenter is used but in slightly different way:

- (void) send:(NSString*)name data:(NSDictionary*)data
{
    NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter];
    NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:nil];
    NSString* json = [NSString.alloc initWithData:jsonData encoding:NSUTF8StringEncoding];
    [center postNotificationName:name object:json userInfo:nil deliverImmediately:YES];
}

Notice that the JSON string is sent as the object of the notification, and not in the userInfo. That is necessary for these notifications to work properly.

Restarting FinderSyncExtension on app launch

Sometimes, it may be useful to restart the extension when your MainApp is launched. To do that, execute the following code when MainApp launches (i.e. in didFinishLaunchingWithOptions method):

+ (void) restart
{
    NSString* bundleID = NSBundle.mainBundle.bundleIdentifier;
    NSString* extBundleID = [NSString stringWithFormat:@"%@.FinderSyncExt", bundleID];
    NSArray<NSRunningApplication*>* apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:extBundleID];
    ASTEach(apps, ^(NSRunningApplication* app) {
        NSString* killCommand = [NSString stringWithFormat:@"kill -s 9 %d", app.processIdentifier];
        system(killCommand.UTF8String);
    });
 
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSString* runCommand = [NSString stringWithFormat:@"pluginkit -e use -i %@", extBundleID];
        system(runCommand.UTF8String);
    });
}

Debugging

Debugging the FinderSyncExtension is pretty straightforward. Some options are described below.

Debugging with Xcode alone

It is possible to debug both MainApp and FinderSyncExtension simultaneously. First, start the MainApp running the Xcode target. Then, set the FinderSyncExtension scheme and run it.
Set breakpoints in desired places in the MainApp source and in the FinderSyncExtension source.
Sometimes, the FinderSyncExtension debug session may not be attached to the relevant process. In that case, it helps to relaunch the Finder: press Alt+Cmd+Escape to bring Force Quit Application dialog and then select the Finder and relaunch it.

macOS Force Quit dialog

Xcode should now attach the debug session properly to the new process.

Debugging with AppCode + Xcode

If you’re using AppCode, then you can launch the MainApp form AppCode and FinderSyncExtension from the Xcode. This way, you can see both logs and debug sessions a bit easier.

Troubleshooting

It could happen that, even though the MainApp and FinderSync processes are running, no file sync statuses are shown. It can also happen that the requestBadgeIdentifierForURL method is not being called at all.
If that happens, check if you have other FinderSyncExtensions running on your MBP (ie Dropbox, pCloud…). You can check that in System Preferences -> Extensions -> Finder.
Disable all extensions except your demo FinderSyncExtension and then see if the issue is still present.

Testing

It seems that there is not much room when it comes to testing the FinderSyncExtension. At the time of writing this post, the only way to test the extension would be to refactor the code into a framework and then have the framework tested.

Conclusion

FinderSyncExtension is a great way to show file sync statuses. Hopefully, you now have a better understanding on how to develop the extension. Solutions shown in this article are designed to be simple yet powerful enough to face real-world use cases.

Useful links

Demo project on bitbucket
Finder Sync Extension
FinderSync Class
Human Interface Guidelines
Distributed Notifications
Inter-Process Communication
JNWThrottledBlock – Simple throttling of blocks
Open source project using mach ports for bidirectional communication

Tags

Marko Cicak

codecentric Novi Sad employee since April 2012. Working as part of the CenterDevice iOS and macOS development team.

Comment

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