Finding a dynamic framework linking solution for Flint

We found during development of a client app built with Flint, that App Store submissions would get rejected during processing for the Info.plist not containing some required usage description keys Contacts and Calendars access, even though the app did not use these frameworks.

Internally Flint has a suite of PermissionAdapter classes that implement both the verification of and requesting permission from the user. Some of these were the culprit as they need to access the APIs of system frameworks just to see if they are authorised or not.

App Store processing will detect these trivial usages and assume you need to supply a description, even if you never use those permissions in your Flint-based app. What we had in the code was:

#if canImport(Contacts)
import Contacts
#endif

Later on we’d execute some logic to e.g. ask the CNContactStore if it was authorized. We never did this unless you actually required the contacts permission in your app, so this code path was never hit, but the App Store processing isn’t smart enough to know this, saw that we had linked against the Contacts framework and instantiated CNContactStore, and so rejected us. We have no desire to bypass App Store protections for bad apps, but in this case we’re just trying to make things easier for devs, and if they want to use Contacts or EventKit in their apps, they will have to provide a description anyway.

What we need to achieve is the ability to check for permission authorisation on an API only if your app has linked and used that API already, so we can pass App Store verification even though we reference other frameworks your app does not use.

Somewhere between my optimism, naïvety, a long period in the 2000s writing Java, Groovy and JS code, and my general disdain for the horrible technical details of compiling and linking, I misunderstood what Swift’s #if canImport(<framework-name>) does. Thanks to the ever-helpful, patient and smart @michel_fortin on the Core Intuition Slack I was put right.

The Swift canImport directive simply checks if the specified framework is available to the compiler when building your code. Because Flint is a dynamic framework built separately from your app by Carthage, it has access to all the frameworks in your build chain. As a result that code will always compile in to Flint and your app gains transitive dependencies on Contacts and others, even if it never uses the permissions.

It’s important to understand that importing the module does not create the linker dependency that the App Store picks up on for the rejection — it is the instantiation of a class like CNContactStore. This instantiation was within #if canImport(Contacts) blocks in the code, but as we learned above that doesn’t help us as Contacts is always available at compile time.

So another solution was required. All of the possibilities are centered around weakly linking the target frameworks — yes, individually for every framework for which we have a permission adapter.

A few options exist for this, and I implemented a few variations of them while researching a workaround.

Using the Objective-C runtime to bind methods at runtime

You can of course use the Objective-C runtime to dynamically instantiate the “manager” type such as CNContactStore or EKEventStore, and then use those same runtime APIs to get references to the functions to get the current authorisation status and request access. Using NSClassFromString and then instantiating them is easy enough thankfully they have no-arg init initialisers. This is a dirty business, and nobody likes it.

Binding all the functions like this is really ugly in Swift. One example cited Mixpanel framework’s conditional use of the advertising identifier only if the AdSupport framework is linked.

I ended up making some reusable dynamic function binding code in Swift but it was still ugly to use, relying heavily on some generics and function currying. I learned some things, so it wasn’t totally wasted time:

struct DynamicInvocation {
    let instance: AnyObject
    let method: IMP
    let selector: Selector

    init(instance: AnyObject, method: IMP, selector: Selector) {
        self.instance = instance
        self.method = method
        self.selector = selector
    }

    init(className: String, staticMethodName: String) throws {
        guard let targetClass = NSClassFromString(className) else {
            throw DynamicBindError.classNotFound
        }
        instance = targetClass
        selector = NSSelectorFromString(staticMethodName)
        guard let method = targetClass.method(for: selector) else {
            throw DynamicBindError.methodNotFound
        }
        self.method = method
    }
    
    init(object: AnyObject, methodName: String) throws {
        instance = object
        selector = NSSelectorFromString(methodName)
        guard let method = object.method(for: selector) else {
            throw DynamicBindError.methodNotFound
        }
        self.method = method
    }
    
    /// This is the evil part. Cast the C function to a Swift
    /// function type. Remember to add `@convention©` to your 
    /// function type!
    func perform<T, ReturnType>(block: (_ functionGenerator: () -> T, _ instance: AnyObject, _ selector: Selector) -> ReturnType) -> ReturnType where T: Any {
        let f: () -> T = {
            return unsafeBitCast(self.method, to: T.self)
        }
        return block(f, instance, selector)
    }
}

func instantiate(classNamed className: String) throws -> NSObject {
    guard let targetClass = NSClassFromString(className) as? NSObject.Type else {
        throw DynamicBindError.classNotFound
    }
    return targetClass.init()
}

/// Example usage, where the FuncType determines the type we expect back from the unsafe cast.
/// This will bind to a function taking no args and returning Int
func dynamicBindIntReturn(toStaticMethod methodName: String, on className: String) throws -> () -> Int {
    let invocation = try DynamicInvocation(className: className, staticMethodName: methodName)
    return {
        typealias FuncType = @convention(c) (AnyObject, Selector) -> Int
        return invocation.perform { (functionGenerator: () -> FuncType, instance, selector) in
            let function: FuncType = functionGenerator()
            return function(instance, selector)
        }
    }
}

This worked, in that it removed the hard dependencies on the frameworks in most cases, but it is really unwieldy. It got really nasty when I got to CoreMotion, where Flint needs to call a function to query activity just to see if it has permission to access motion data — and that function takes several arguments as well as a handler that has several arguments. The code smell was getting too much to stomach.

This approach also adds a lot of code dependency on the ObjC runtime and a lot of generics-related bloat to the binary.

Using a dummy @objc protocol and unsafe casting 

It sounds scary and more hacky, but Michel suggested this and it works well. Basically, we create an @objc protocol that duplicates only the parts of the class we need to interact with, and after dynamically creating an instance at runtime as an AnyObject, we unsafeBitCast it to our protocol type. Due to Objective-C dynamic method resolution this all works as it should:

@objc enum ProxyAVAuthorizationStatus: Int {
    case notDetermined
    case restricted
    case denied
    case authorized
}

@objc protocol ProxyCaptureDevice {
    // We don't mark these static as we call them on the class itself.
    @objc(authorizationStatusForMediaType:)
    func authorizationStatus(for mediaType: AVMediaType) -> ProxyAVAuthorizationStatus
    @objc(requestAccessForMediaType:completionHandler:)
    func requestAccess(for mediaType: AVMediaType, completionHandler handler: @escaping (Bool) -> Void)
}

The fun gotcha part here is that this protocol does not use the Objective-C style naming so you get unrecognised selector warnings, until you add the @objc names.

A final little challenge there is that calling a static function on the protocol itself isn’t possible, even when you have done the unsafe cast to the real ObjC type. You need to get the type of the actual class instead of an instance, and call it on there by casting the type to the protocol instead:

lazy var captureDeviceClass: AnyObject = { NSClassFromString("AVCaptureDevice")! }()
lazy var proxyCaptureDeviceClass: ProxyCaptureDevice = { unsafeBitCast(self.captureDeviceClass, to: ProxyCaptureDevice.self) }()

So for the AVCaptureDevice and a few others, we get the class rather than instantiating the type, and mark the protocol functions as instance functions, even though in Objective-C they are static functions.

And yet, Contacts and EventKit are still causing rejections

It seems Contacts and EventKit get special treatment during app processing compared to the other authorisation APIs. We’d used the smart casting technique as a relatively lightweight solution to the weak linking, but were still getting rejected for missing NSContactsUsageDescription and NSCalendarsUsageDescription keys.

It turns out you flat out can’t use Objective-C selectors that match the requestAccessXXXX functions of CNContactStore and EKEventStore, even if these are on your own protocol types! This really is quite evil, as you could easily create your own APIs that clash with this naming. (I suspect this is why the selectors differ between Contacts and EventKit also, so they can filter each API separately)

As a result, we could only get past processing when we removed the @objc proxy protocol technique for Contacts and Events permission adapters only, and revert them to pure dynamic class and method resolution using strings. Yes — the string selector names for these do not cause rejection. This is the common pattern used for weak linking AdSupport and advertisingIdentifier in other frameworks for analytics and so on.

At least we didn’t need to duplicate the types

You can still import the framework without creating a dependency as long as you don’t reference any class types specific to that framework. Enums, constants and closure types are all baked in at compile time… and if as in some cases there are completion closures that take arguments that use classes from the framework, you can substitute them for NSObject or AnyObject if you don’t need them.

At least that was what we hoped. It turns out at least some system frameworks have Swift-optimized shims called e.g. libswiftPhotos.dylib which are automatically added to your link dependencies when building, and embedded in the app.

The mystery of Contacts always being imported

It took a really long time to figure this out — having removed the import Contacts and hence all references to CN-prefixed types in the framework. We were still being rejected on submission for missing NSContactsUsageDescription.

It turns out that if you import Intents for the Siri Kit intents APIs — e.g. for the interaction property on NSUserActivity — that libswiftIntents.dylib will be automatically added to your dependencies. That shouldn’t be a problem, of course.

However this dylib itself brings in a viral dependency on Contacts, which is most unexpected and most likely an Apple bug. I created a radar #41946218 for this, as this is going to cause us a lot of problems adopting the new Siri functionality.

Unless this changes when we add iOS 12 Siri functionality to Flint, all users of Flint will to have to supply NSContactsUsageDescription event if their code never touches or explicitly imports Contacts. We will have to import types like INShortcut and INIntent to achieve the things we want, and right now that forces a dependency on Contacts - dating back to at least iOS 11 and Xcode 9.4, by the way.

Other possible avenues for the future

One possible solution I explored but didn’t try to implement yes, is to use whole-framework weak linking where you turn off all auto-linking and specify -weak_framework <framework-name> for every framework used by the permissions adapters, as part of the “Other linker flags” settings. This would still likely require the dynamic instantiation of “manager” types like the other solutions, but it is not clear if Carthage would honour these settings when building the framework. I’ll investigate another time as this could be an even cleaner solution, but I suspect App Store processing will even reject weak linking without the other code changes we made. I note that Apple’s own CareKit seems to advocate this approach.

Aside from this there is another option to consider: use Swift compiler defines to completely remove references to these frameworks in the code, e.g. FLINT_CONTACTS when defined could bring in all the code that requires “Contacts”, and stub it all out when not set. The issue here is that Carthage has to be told all of these definitions, using an XCODE_CONFIG_FILE environment variable set point at an xcconfig with a list of values such as:

OTHER_SWIFT_FLAGS="-DFLINT_CONTACTS -DFLINT_EVENTS"

This will be pretty ugly for developers to set up however, and you couldn’t just check out your code and build without doing some scripting or environment setup.

For the moment I decided not to go with that because of the configuration hassles and how easy it would be to get confused when you add events permissions to a feature and yet your app continues to tell you that Events permissions are not supported because the last Carthage build you ran did not include the compiler define for it.

In closing…

This was a rather horrible mission and a brain teaser in the end. I think we’ve got a workable solution for the moment and we’re successfully submitting apps to the store, thanks to some great suggestions from the ever-supportive people on the Core Intuition Slack.

We plan to look at a less hacky solution, and to talk with the Swift community about the right approach for this in the future, as Apple surely needs this solved as we move away to Swift-only frameworks. We will also be looking to remove the linking of dependencies for other frameworks such as Photos unless you have added Photos to your app, to reduce linking times at startup.

If you have thoughts or recommendations on this please get in touch!


We're now tagging, plus new advice & error handling functions

ea-1.0.1 release helps troubleshooting of problems when calling Flint

Early access release 1.0.2

Fix for Contacts dependency and improved perform() function signature