FileProvider extensions Mac Catalyst availability and workarounds

A have the application with iOS and Mac Catalyst versions and I need to make a cloud client for the app's documents. FileProvider would be the great choice for this feature, but I can't believe it doesn't support Mac Catalyst.

At this moment I'm almost certain that NSFileProviderReplicatedExtension does not support Mac catalyst officially. And if it so, It would be great to hear the exact status and future plans if any.

Unofficially, I managed to run it.

  1. I switched the extension's target Supported Destination from Mac Catalyst to Mac and it started to compile. This move seems legit to me.

  2. But domain also had to be created, and this part was a way trickier. I've added new bundle to host app(iOS and catalyst), but with supported platform - macOS in build settings. There I created an NSObject subclass DomainManager which calls NSFileProviderManager's addDomain method in its createDomainIfNeeded(), which is also exposed in public extension to NSObject - a kind of "informal protocol"

  3. The catalyst app creates bundle by name and loads principal class (DomainManager), but as NSObject reference, and then calls createDomainIfNeeded() method on it.

The location defined by domain appears in Finder sidebar, and the dataless item "a file" appears in this location, as defined by stub implementation in the extension enumerator method. This means file system instantiated the extension instance under Mac catalyst and called the protocol method on it. I.e. it seem to work.

But the question is whether this solution is stable and legit for the App Store distribution. Or is it pandora box with unforeseeable consequences for user data? Thanks in advance.

Answered by DTS Engineer in 799202022

A have the application with iOS and Mac Catalyst versions and I need to make a cloud client for the app's documents. FileProvider would be the great choice for this feature, but I can't believe it doesn't support Mac Catalyst.

At this moment I'm almost certain that NSFileProviderReplicatedExtension does not support Mac catalyst officially.

Yes, though I think this is really because of a combination of history and general "context":

  1. Historically, NSFileProviderReplicatedExtension was supported on macOS before it was supported by iOS. It would have been slightly odd to add a macOS "compatibility" layer for an API that macOS already supported.

  2. Catalyst's primary goal/role is to allow you to use UIKit to write macOS apps, but NSFileProviderReplicatedExtension is the kind of extension point that shouldn't be doing ANY interface work (AppKit OR UIKit).

That second point is the critical one here. There are extension points (Notification Service Extension (NSE) is another example*) where exactly the same code would work fine as a both an "iOS project" and a "macOS project" simply because the extension only used APIs that were already available on both platforms.

*Note that the Notification Content Extension (NCE) DOES supports mac catalyst while the NSE does not. The NCE involves interface work (where Catalyst is useful) while the NSE does not.

One other point that isn't always clear in our documentation- the different framework environments we have (macOS, SwiftUI, macCatalyst) are entirely about how the particular target/"object"/component is built, NOT how the different components that make up an app are built. In concrete terms, an app written in macCatalyst should not be calling AppKit APIs. However, there isn't any issue at all with an app written in macCatalyst embedding app extension written in AppKit or SwiftUI.

But the question is whether this solution is stable and legit for the App Store distribution. Or is it pandora box with unforeseeable consequences for user data? Thanks in advance.

It's always tricky to follow exactly what's being done in a text description, however, there isn't any issue with a mac catalyst app including a macOS app extension inside it. This is also the configuration I'd use to support NSFileProviderReplicatedExtension*.

*In theory you might be able to use EXACTLY the same extension on macOS and iOS, but in practice I think you'll end up with different implementations because of differences in usage patterns and user expectations/requirements.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

A have the application with iOS and Mac Catalyst versions and I need to make a cloud client for the app's documents. FileProvider would be the great choice for this feature, but I can't believe it doesn't support Mac Catalyst.

At this moment I'm almost certain that NSFileProviderReplicatedExtension does not support Mac catalyst officially.

Yes, though I think this is really because of a combination of history and general "context":

  1. Historically, NSFileProviderReplicatedExtension was supported on macOS before it was supported by iOS. It would have been slightly odd to add a macOS "compatibility" layer for an API that macOS already supported.

  2. Catalyst's primary goal/role is to allow you to use UIKit to write macOS apps, but NSFileProviderReplicatedExtension is the kind of extension point that shouldn't be doing ANY interface work (AppKit OR UIKit).

That second point is the critical one here. There are extension points (Notification Service Extension (NSE) is another example*) where exactly the same code would work fine as a both an "iOS project" and a "macOS project" simply because the extension only used APIs that were already available on both platforms.

*Note that the Notification Content Extension (NCE) DOES supports mac catalyst while the NSE does not. The NCE involves interface work (where Catalyst is useful) while the NSE does not.

One other point that isn't always clear in our documentation- the different framework environments we have (macOS, SwiftUI, macCatalyst) are entirely about how the particular target/"object"/component is built, NOT how the different components that make up an app are built. In concrete terms, an app written in macCatalyst should not be calling AppKit APIs. However, there isn't any issue at all with an app written in macCatalyst embedding app extension written in AppKit or SwiftUI.

But the question is whether this solution is stable and legit for the App Store distribution. Or is it pandora box with unforeseeable consequences for user data? Thanks in advance.

It's always tricky to follow exactly what's being done in a text description, however, there isn't any issue with a mac catalyst app including a macOS app extension inside it. This is also the configuration I'd use to support NSFileProviderReplicatedExtension*.

*In theory you might be able to use EXACTLY the same extension on macOS and iOS, but in practice I think you'll end up with different implementations because of differences in usage patterns and user expectations/requirements.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you for the confirmation of the extension target setup. Here are more details to the problem part:

So, to get working cloud client solution we require 2 parts: extension(check) and app. And in the app part we have to create the FP domain and in simplified form it may look like this (screenshot) The domain creation doesn’t depend on UI, really, but it is unavailable for Catalyst, and this is the Problem.

The workaround solution we’ve found is next: We’ve added a bundle in the app, and moved DomainManager there. Target platform set for this bundle is Mac. But the app code(Catalyst) can’t link to the code(Mac) in this bundle and call it directly. So we load bundle, instantiate principal class (DomainManager in runtime) as NSObject instance and call our NSObject category method on it, and the DomainManager’s version gets called.


class DomainManagerConnector {
    func makeMacOSCodeAddDomain() {
        if let bundleURL = Bundle.main.url(forResource: "NativeOnlyBundle", withExtension: "bundle"),
           let myBundle = Bundle(url: bundleURL) {
            if myBundle.load() { // Load the executable code
                // Access the principal class of the bundle, if defined
                if let principalClass = myBundle.principalClass as? NSObject.Type
                {
                    let managerInstance = principalClass.init()
                    managerInstance.***_addDomain()
                }
            }
        }
    }
}

@objc public extension NSObject {
    func ***_addDomain() {
    }
}

I’m just not sure if such workaround will be stable and if it’s legit for the App Store distribution.

Accepted Answer

The workaround solution we’ve found is next: We’ve added a bundle in the app, and moved DomainManager there. Target platform set for this bundle is Mac. But the app code(Catalyst) can’t link to the code(Mac) in this bundle and call it directly.

So, the first thing I would actually figure out here is how you actually want your larger "product" to work on macOS. It's possible that you really do want just 2 components (app and extension), but most of this products on the mac end up with 3:

  1. The app
  2. The extension
  3. An agent/login item which provides a minimal UI (typically a menu extra) and also manages network activity for 1 & 3.

This is different that how iOS would work, but that's only because iOS doesn't provide anyway to implement #3.

In any case, in the 3 component architecture above #3 would be written as "native" macOS (not catalyst) and would also have all of the NSFileProvider code. #1 would then be written in macCatalyst and do all of it's work "through" #3.

So we load bundle, instantiate principal class (DomainManager in runtime) as NSObject instance and call our NSObject category method on it, and the DomainManager’s version gets called.

No, this isn't something I'd really recommend. The approach above is actually a very old technique that had generally fallen out of favor for many, many years before macCatalyst "revived" it as a way to hack around the fact that specific classes/object were missing from macCatalyst. It's main upside is that it's really easy, it's main downside (in the case of macCatalyst) is that it ends up creating system object in a runtime environment that they may not operate correctly in. Note that the issue with this kind of technique ISN'T that it's going to break "now" or otherwise fail. If that were the problem, then you could just "test enough" and be ok. The problem is that it can work fine NOW, then break later because we made changes that relied on details of the underlying runtime. In the worst case, this can basically mean you have rewrite the entire implementation when the "proper" approach (see below) ends being the only approach that works.

The better approach here is to keep the NSFileProvider code in separate process which can execute in the standard macOS runtime (not Catalyst). That would be the agent/login above (if you're going to use that architecture anyway), but it could also be a simple XPCService if you ONLY want two components.

I’m just not sure if such workaround will be stable and if it’s legit for the App Store distribution.

My technical objections are above.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

FileProvider extensions Mac Catalyst availability and workarounds
 
 
Q