ExtensionKit is a pretty significant new feature of macOS Ventura. But, I wouldn’t be surprised if you didn’t know, as it had a conspicuously quiet introduction. There were no sessions or labs about it during WWDC 2022. I only discovered it because a friend stumbled across the beta documentation and sent it to me.
But whew, did it arrive at the right time. We’d just finished up adding Ruby support. It was a big project for us, and really highlighted how much we needed a better system for expanding Chime. We had just begun investigating how we could build a real extension system when ExtensionKit came along. We decided pretty quickly to move to adopt it, and we now use it as the basis for ChimeKit.
And, now that ChimeKit’s out, we thought we’d take a moment to write a little about our experiences with ExtensionKit. But, it turned out to be a big topic so we’re doing a series. This is the first post, really just focusing on getting familar with what ExtensionKit is all about.
It appears that ExtensionKit, and the lower-level ExtensionFoundation have been around for quite a while. All of the iOS and macOS extension features are based on it. But, it also looks like Apple has been using this system internally as well. This bodes well for any framework, and ExtensionKit has indeed been pretty solid.
At a high-level, you can define extension points in your app, either with or without a UI component. All communication between extension and host goes over XPC, and there’s a bit of infrastructure provided by Apple for discovering available extensions and establishing a connection.
One of the most exciting things ExtensionKit can do is remote views. This is a view that is constructed with SwiftUI and managed within the extension, but displayed within the hosting application. As far as I can tell, this arrangement is totally transparent and supports virtually everything that SwiftUI can do, even animation. Perhaps the only real downside is window/view resizing can sometimes have a little lag.
Communication between extension and host is all done using XPC. There’s one global XPC connection per extension, but there’s also an optional connection per UI scene. At first, this was surprising to me. But, once I started actually experimenting with UIs, I realized this arrangement makes a lot of sense. While there will be only one extension instance, an extension can be asked to render multiple instances of its views. Of course, your design may not support this. But, if you do, this is a good means of coordination.
Extension hosts (ie app developers) will almost certainly want to provide some kind of framework that defines a higher-level API for extension-host interaction. This is an area that demands some careful work. First, you want to provide a reasonable API for extension developers. But, you also need to consider forwards- and backwards-compatibility. There’s nothing provided by ExtensionKit to keep these APIs in sync across the versions of extensions and app that your users have installed.
ExtensionKit will refuse to load any extensions that do not have the sandbox entitlement set. How much of a problem this is really depends on what your extension developers need to do. Fortunately, if it is a problem, you have some options.
It is possible to pass URL bookmark data across the XPC connection to share access. I’ve found it pretty confusing to understand how to use the security features of bookmarks, but in this case, it’s proven to be easy.
// create the data on the side that has access let data = try url.bookmarkData() // a side-effect of resolving it on the other end will grant the process access var stale: Bool = false let _ = try URL(resolvingBookmarkData: data, options: [.withoutUI], relativeTo: nil, bookmarkDataIsStale: &stale)
If sharing file access isn’t sufficient, and it definitely is not for Chime, you’ll need to turn to other methods. One option is a library we’ve open sourced called ProcessService. It is a system for executing processes outside of a sandbox via an XPC service.
An interesting quirk of ExtensionKit is that extensions must be bundled within a
.app. They cannot be distributed as standalone artifacts. I think this makes a lot of sense when you are attempting to expose functionality of an existing application in the form of an extension. But, there are lots of reasons why you might want to just make the extension itself. This even makes sense for Apple’s own supported use-cases, like Safari and Xcode editor extensions. And, Chime definitely falls into a similar category.
Now, you totally can just make a little wrapper app that does nothing but contain your extension. You can even get these minimal, placeholder apps onto the App Store. But, it’s just a pain. So, we’re planning on putting together a simple first-party “Extension Gallery” application. We’ll handle the details. All an extension developer will have to do is structure the extension as an open-source library.
This is probably more work than most extension-hosting applications will do. But, extensions are going to be a core part of Chime going forward, so it makes sense for us.
Regardless of how you get an extension onto a user’s machine, there’s one more required step before they can be used. Extensions have to be user-approved via
EXAppExtensionBrowserViewController. This controller presents a UI that displays all available extensions, organized by containing application. You’ll have to find a place to put this view within your hosting app. And, perhaps adding to the integration complexity, users can approve and revoke that approval at any time.
Requiring user approval is reasonable. And, I’m really happy to say that extensions bundled within the hosting app itself are auto-approved. This could be an atypical arrangement, but something that’s worth knowing just in case.
Right now, hosting ExtensionKit extensions can only be done from macOS. But, as far as I can tell, all of the extension-side APIs are available to iOS, tvOS and watchOS as well. I think this, combined with the extremely conspicuous lack of WWDC sessions means ExtensionKit was supposed to ship with iOS 16, but was pulled at the last minute.
I’ll also submit as evidence
EXAppExtensionBrowserViewController.h as found in Xcode 14.0 beta 6:
#if !TARGET_OS_WATCH #if TARGET_OS_OSX #import <AppKit/AppKit.h> #else #import <UIKit/UIKit.h> #endif // TARGET_OS_OSX NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(macos(13.0)) API_UNAVAILABLE(ios, watchos, tvos) EXTENSIONKIT_EXPORT #if TARGET_OS_OSX @interface EXAppExtensionBrowserViewController : NSViewController @end #else @interface EXAppExtensionBrowserViewController : UIViewController @end #endif // TARGET_OS_OSX NS_ASSUME_NONNULL_END #endif // !TARGET_OS_WATCH
Today, I’m not sure you can do too much with ExtensionKit on non-macOS platforms. But, when you consider just how many entirely new types of apps could be built with it, just the idea is exciting.
Now, of course ExtensionKit does have some pretty compelling features. It’s all Swift, providing easy access to all of Apple’s first-party APIs. Its remote view capabilities are really amazing. And, it offers a way for 3rd-party apps to integrate with each other in a way that is straightforward and well-supported. I’m sure all of these things are possible with another approach, but ExtensionKit makes them all easy.
I certainly wasn’t expecting ExtensionKit, but I’m really happy it’s here. It’s made working on ChimeKit really fun, and we’re excited about what it makes possible.
ExtensionKit, and our use of it, turned out to be a pretty big topic. This is just an introduction, but hopefully its gotten you interested in learning more. Next up, we’ll be getting into details on the communication infrastructure we used to build an ExtensionKit-based framework.
Wed, Aug 17, 2022 - Matt Massicotte