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”.
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.
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.
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
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 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
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:
1NSString* observedObject = self.mainAppBundleID; 2NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; 3 4[center addObserver:self selector:@selector(observingPathSet:) 5 name:@"ObservingPathSetNotification" object:observedObject]; 6 7[center addObserver:self selector:@selector(filesStatusUpdated:) 8 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:
1NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; 2[center postNotificationName:name 3 object:NSBundle.mainBundle.bundleIdentifier 4 userInfo:data 5 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.
Sending messages from FinderSyncExtension to MainApp
To send a message from FinderSync to the MainApp, NSDistributedNotificationCenter is used but in slightly different way:
1- (void) send:(NSString*)name data:(NSDictionary*)data 2{ 3 NSDistributedNotificationCenter* center = [NSDistributedNotificationCenter defaultCenter]; 4 NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:nil]; 5 NSString* json = [NSString.alloc initWithData:jsonData encoding:NSUTF8StringEncoding]; 6 [center postNotificationName:name object:json userInfo:nil deliverImmediately:YES]; 7}
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):
1+ (void) restart 2{ 3 NSString* bundleID = NSBundle.mainBundle.bundleIdentifier; 4 NSString* extBundleID = [NSString stringWithFormat:@"%@.FinderSyncExt", bundleID]; 5 NSArray<NSRunningApplication*>* apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:extBundleID]; 6 ASTEach(apps, ^(NSRunningApplication* app) { 7 NSString* killCommand = [NSString stringWithFormat:@"kill -s 9 %d", app.processIdentifier]; 8 system(killCommand.UTF8String); 9 }); 10 11 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 12 NSString* runCommand = [NSString stringWithFormat:@"pluginkit -e use -i %@", extBundleID]; 13 system(runCommand.UTF8String); 14 }); 15}
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.
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
More articles
fromMarko Cicak
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Marko Cicak
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.