Search code examples
iosswiftselectoriphone-privateapi

Passing closures to Private API's


I'm trying to fetch all the available airplay devices from the private API MPAVRoutingController. I'm using a third party perform selector library for swift called performSelector-Swift. The method I am trying to call is fetchAvailableRoutesWithCompletionHandler. This takes one parameter, an objective-c block. - (void)fetchAvailableRoutesWithCompletionHandler:(id /* block */)arg1; When I try and pass in a closure I get a compile error and if I don't pass anything in my app crashes. I'm not releasing this app and thats why I'm using the priv API.

let MPAVRoutingController = NSClassFromString("MPAVRoutingController")! as! NSObject.Type
let routingController = MPAVRoutingController.init()
if let availableRoutes = routingController.swift_performSelector("fetchAvailableRoutesWithCompletionHandler:", withObject: {
        object in
    }) {
        print(availableRoutes)
    }

Solution

  • First.. How I found the correct completion block signature: https://i.sstatic.net/fApqg.png

    That shows that it allocates an NSMutableArray as the parameter to the completion block when it invokes it. That's the only parameter. You don't have to do this (disassemble it). Upon an exception being thrown, you can print the signature. Sometimes it will also tell you which kind of block is expected.


    Next, my opinion on invoking selectors dynamically..

    Your best option is to not perform selectors.. It's a pain especially when the call contains MULTIPLE parameters..

    What you can do is invocation through interface/extension pointers.. I do this in C++ (Idea from the Pimpl idiom.. COMM interfaces do this too) all the time and it works with Swift, Objective-C, Java.. etc..

    Create a protocol that has the same interface as the object. Create an extension that inherits that protocol. Then cast the object instance to that extension/interface/protocol.

    Call whatever function you want via the interface/extension/protocol pointer.

    import UIKit
    import MediaPlayer
    
    @objc
    protocol MPAProtocol { //Functions must be optional. That way you don't implement their body when you create the extension.
        optional func availableRoutes() -> NSArray
        optional func discoveryMode() -> Int
        optional func fetchAvailableRoutesWithCompletionHandler(completion: (routes: NSArray) -> Void)
        optional func name() -> NSString
    }
    
    extension NSObject : MPAProtocol { //Needed otherwise casting will fail!
    
         //Do NOT implement the body of the functions from the protocol.
    }
    

    Usage:

    let MPAVRoutingControllerClass: NSObject.Type = NSClassFromString("MPAVRoutingController") as! NSObject.Type
    let MPAVRoutingController: MPAProtocol = MPAVRoutingControllerClass.init() as MPAProtocol
    
    MPAVRoutingController.fetchAvailableRoutesWithCompletionHandler! { (routes) in
        print(routes);
    }
    

    If you were to do it with a Bridging header instead of creating the extension + protocol, you'd just do a single Objective-C category:

    #import <Foundation/Foundation.h>
    
    @interface NSObject (MPAVRoutingControllerProtocol)
    - (void)fetchAvailableRoutesWithCompletionHandler:(void(^)(NSArray *routes))completion;
    @end
    
    @implementation NSObject (MPAVRoutingControllerProtocol)
    
    @end
    

    Then:

    let MPAVRoutingControllerClass: NSObject.Type = NSClassFromString("MPAVRoutingController") as! NSObject.Type
    let MPAVRoutingController = MPAVRoutingControllerClass.init()
    
    MPAVRoutingController.fetchAvailableRoutesWithCompletionHandler! { (routes) in
        print(routes);
    }
    

    Finally, if you can use protocol injection, you can do this much easier:

    func classFromString(cls: String, interface: Protocol?) -> NSObject.Type? {
        guard let interface = interface else {
            return NSClassFromString(cls) as? NSObject.Type
        }
    
        if let cls = NSClassFromString(cls) {
            if class_conformsToProtocol(cls, interface) {
                return cls as? NSObject.Type
            }
    
            if class_addProtocol(cls, interface) {
                return cls as? NSObject.Type
            }
        }
        return nil
    }
    
    func instanceFromString<T>(cls: String, interface: Protocol?) -> T? {
        return classFromString(cls, interface: interface)?.init() as? T
    }
    
    
    
    @objc
    protocol MPAProtocol {
        optional func availableRoutes() -> NSArray
        optional func discoveryMode() -> Int
        optional func fetchAvailableRoutesWithCompletionHandler(completion: (routes: NSArray) -> Void)
        optional func name() -> NSString
    }
    
    
    let MPAVRoutingController: MPAProtocol = instanceFromString("MPAVRoutingController", interface: MPAProtocol.self)!
    
    MPAVRoutingController.fetchAvailableRoutesWithCompletionHandler! { (routes) in
        print(routes);
    }