Calling Hidden/Private API from Swift in Style

A simple, clean, and safe way to call hidden API in Swift 5

Mhd Hejazi
The Startup

--

Photo by Gwendal Cottin

Making private API visible in Swift is a tedious job. You have to find the private headers, copy them to your project, create a bridging header file, and import the private headers. Or you can use message sending techniques to perform a method selector on a target object, and extract the returned value and convert it to a Swift type.

In this post, we’ll see how we can use the Swift attributes @dynamicMemberLookup and @dynamicCallable to create a wrapper around Objective-C classes and objects, and then use method invocation to access their properties and methods. By doing so, we’ll be able to call private API in a far more simple and intuitive way, and pure Swift.

Background

Assume we have the following private Objective-C class that we want to access in Swift:

Toolbar class defined in Objective-C

There are three ways to dynamically call the method in this class:

1. Using performSelector()

But it comes with multiple limitations:

  • You can’t use it if one of the parameters, or the return type, is a number, boolean, or a struct value. It only works with objects since its parameters and return type are defined as NSObject *.
  • You also can’t use performSelector() if the method you want to call has more than two parameters.

2. Using methodForSelector() with @convention(c)

This is a more flexible solution, but you have to define the method type and the selector, and cast the method implementation to the defined type for every API call you want to make.

3. Using NSInvocation

This class is only available in Objective-C and can’t be used from Swift, so the example will be in Objective-C:

Every solution mentioned above has its limitations, not to mention how ugly and clunky all of them are. We need a better solution that allows us to write cleaner code with minimal syntax. Something like this:

But there is no existing solution like that, so let’s create one!

The dynamic solution

What we’re trying to do now is creating a class that will wrap the target object and allows us to call the method directly from it as if the method was actually defined in the wrapper class itself.

But how will the compiler allow us to access a property or call a method that is not defined in the first place?

Well, thanks to the latest additions in Swift 4.2 and Swift 5.0, this is now possible with @dynamicMemberLookup and @dynamicCallable attributes.

1. @dynamicMemberLookup

This attribute allows using the dot syntax to access arbitrary properties. With this attribute, the access to an undefined property is translated to a call to the dynamic member subscript.

Let’s add it to our wrapper class:

Now, with this attribute, we can write something like dynamic.foo and the compiler will convert this unknown property access into a call to our dynamic subscript getter:

dynamic[dynamicMember: "foo"]

Note how we made the subscript returns a Dynamic object which will allow us to chain calls and do things like dynamic.foo.bar.

2. @dynamicCallable

This attribute marks a type as being “callable”. Instances of a callable type can be treated as functions that could be “called” directly by adding () after the object name. And the compiler will understand that as a call to a special dynamicallyCall() method we define:

But if you look closely, you’ll notice that the compiler only passes the method arguments to dynamicallyCall() method, so how will we know the method name?

Well, we need to understand first how the compiler translates the complete method call statement.

Assuming that we are calling dynamic.show(item), the compiler will translate this statement to two calls:

How a dynamic method call is translated by the Swift compiler

(1) The dynamic subscript is called first with “show” as the member name.

Remember that our dynamic subscript returns a Dynamic object.

(2) The dynamicallyCall() method is then called from the newDynamic object returned from (1).

Note that we are unable to tell in (1) whether the member name is for a property or a method, and we’ll need to wait until the next dynamic call:

  • If the dynamic subscript is called again, we know it was a property name.
  • if dynamicallyCall() is called, then it was a method name.

This means we’ll always have to delay accessing the actual property from the wrapped object until we are sure the member name was indeed for a property, not a method.

By adding @dynamicMemberLookup and @dynamicCallable attributes, we can now call arbitrary methods from arbitrary properties: dynamic.foo.bar().

3. Method invocation

To complete our solution, we only still have to invoke the actual method from the wrapped object. To do so, we need a solution that allows us to call arbitrary methods with an arbitrary number of arguments.

The first option described above using performSelector() is rejected already since it only allows us to call methods that take no more than two object arguments.

The second option was using methodForSelector() with @convention(c). But to use this solution, one must know in advance the method signature and define its type in code. But this is exactly the opposite of what we're trying to do here — that is calling a method with no prior knowledge of its signature. So this option is also rejected.

This leaves us with the third and last option, NSInvocation. But, as I mentioned above, the class is not even available in Swift. So, how are we going to use it?

Well, we’ll “port” it to Swift ourselves! We can do that by dynamically creating instances of this class, and dynamically calling its methods which will eventually allow us to perform the actual method we are trying to call.

But how can we dynamically call its methods if that's what we're trying to do here in the first place? Well, we can use the second option to define the already known methods of NSInvocation, and call those methods from the dynamically created instance.

I will spare you the details here, but you can find the source code of Invocation, the Swift version of NSInvoation in the GitHub repo mentioned at the end of this post.

And now that we have the Invocation class, we can write the remaining methods:

Dynamic — the library

I improved the Dynamic class and added many features to simplify working with hidden Objective-C classes in Swift furthermore, and converted all of that into a standalone library I published on GitHub under the same name.

Use cases & examples

The main use cases for Dynamic is accessing private iOS and macOS API in Swift. And with the introduction of Mac Catalyst, the need to access hidden API arose as Apple only made a very small portion of the macOS AppKit API visible to Catalyst apps.

What follows are examples of how easy it is to access AppKit API in a Mac Catalyst with the help of Dynamic.

1. Enter fullscreen

Note the difference in some member names (shared vs sharedApplication). The reason is that in Dynamic we’re accessing the Objective-C API, not the Swift version of it. So one has to check the Objective-C method signature from Apple Docs.

Swift method signature
Objective-C method signature

Another difference we can spot is the missing optional chaining operator ? in the second code snippet. The reason is that Dynamic always returns a value when a property is accessed or a method is called. This eliminates the need for dealing with nils as they will always be wrapped with a Dynamic object.

2. Get the NSWindow from a UIWindow

3. Using NSOpenPanel

4. Change the window scale factor

iOS views in Mac Catalyst apps are automatically scaled down to 77%. To change the scale factor we need to access a hidden property.

Bonus: Meta Invocation!

Now that we have theDynamic library, we can create instances of the hidden class NSInvocation and call its methods directly in Swift:

We’re creating an instance of NSInvocation and calling its methods with the help of Dynamic class that uses Invocation, the Swift version of NSInvocation!

I hope this article was helpful. If you have any questions or feedback, feel free to leave a response.

--

--