[SC]()

iOS. Apple. Indies. Plus Things.

Calling Private APIs in iOS

// Written by Jordan Morgan // May 16th, 2022 // Read it in about 4 minutes // RE: Foundation

This post is brought to you by Emerge Tools, the best way to build on mobile.

Invoking private APIs in iOS is thought of as something of a dark art, but in reality - pulling it off is quite simple. There are some practical implications to doing so, despite what many might publicly admit, but perhaps the best reason is that it’s just so much fun.

What follows is a no-nonsense guide to calling private API using…

Objective-C!

…Objective-C?

Why yes, Objective-C. Calling private API is far easier in the language than it would be if we attempted to turn to the modern, safe, statically-typed Swift world, which goes to great lengths to save us from ourselves.

But today, there’s no saving us. We will dive into the dark depths of the Objective-C runtime to perform acts forbidden in Swift. So, don’t be scared - the Dynamic Funtime™️ of Objective-C allows us to use an old friend, NSInvocation, which isn’t available to Swift.

So, if you’re new to calling private API or perhaps you (like me) forget the right incantation to get it working, here’s a short step-by-step guide on how to get going.

Step One - Find API To Invoke

Pick your poison. The best place to go is limneos, and from there - start browsing through header files. In my case, I’ve been messing around with the Tips app recently, so I’ll go here. In particular, I wanna invoke these two class methods from TPKContentView:

+(id)SiriIconWithTraitCollection:(id)arg1;
+(id)TipsIconWithTraitCollection:(id)arg1;

let concatenatedThoughts = """

Keep in mind, iOS is always changing. If you're reading this years from now, the location of this header might've changed. Or, maybe it doesn't even exist anymore. So, heads up.

"""

Step Two - Import a Bundle

Before you can call private API, you need to load the private framework in via bundles. As of today, those are found here:

/System/Library/PrivateFrameworks/ThePrivateFramework.framework.

So, here’s where our code is at1:

- (void)viewDidAppear:(BOOL)animated {
	[super viewDidAppear:animated];
 	
	// Load in private framework
	[[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/TipKit.framework"] load];
 }

Step Three - Create a Class Type

To perform a selector, we’ll need a target too. If you started with Swift and the target-action pattern is new to you, you can check out this post I wrote on the matter about a thousand years ago.

In our case, we’re calling a class function. We don’t need to initialize an instance of TPContentView, so here we can just rely on NSClassFromString() and move on:

- (void)viewDidAppear:(BOOL)animated {
	[super viewDidAppear:animated];
 	
	// Load in private framework
	[[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/TipKit.framework"] load];

	// Get our class type
	Class TPKContentView = NSClassFromString(@"TPKContentView");
 }

Note that if you did need an instance, though - you can certainly do that too, so long as you get the correct selector for the initializer (otherwise, you’ll crash):

// Assumes that `SomePrivateClass` responds to the alloc init message
id tpContentViewInstance = [[NSClassFromString(@"SomePrivateClass") alloc] init];

Even further, if you needed to access a property from anything you initialized, using [theType valueForKey:@”theProperty”] will get you there. But, if it doesn’t exist, (yet again) you’ll crash.

Step Four - NSInvocation

NSInvocation is what we can use to ultimately invoke private API. It’s not the only way, but it is my preferred route.

You can read the docs if you want the full story on it, but here’s the short version: NSInvocation can execute methods dynamically at runtime. You give it a target, selector, arguments and a return value and it’ll construct an Objective-C message object. It’s helpful here because you create one with, well - anything, especially if it’s a class or method you don’t have access to.

That’s what makes its perfect for calling private API. So, looking at our signatures from earlier, we’ve got all we need to create an NSInvocation for them.

This is the most involved step, and it may take a few times to get it right. The process is typically the same, though:

  1. Create selectors.
  2. If the method has a return type, create some variables for those.
  3. Create the invocation object and invoke it.

Here is the entire code sample with those steps added. Read each comment one by one to get a feel for how it all fits together:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    // Load in the private framework
    [[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/TipKit.framework"] load];
    
    // Get our class type
    Class TPKContentView = NSClassFromString(@"TPKContentView");
    
    // Ignore undefined selector warnings below
    // Because clang can't tell me who I am

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
	// Create our two selectors to call
    SEL siriIconSel = @selector(SiriIconWithTraitCollection:);
    SEL tipsIconSel = @selector(TipsIconWithTraitCollection:);
#pragma clang diagnostic pop
    
    // These will be the result of the method invocations
    id returnValSiri;
    id returnValTips;

    // I know these methods take in a trait collection 
    // From trial and error so I'll set it aside here 
    // For readability
    UITraitCollection *traits = self.traitCollection;
    
    // Create the invocations. The first one, for Siri,
    // Is commented step by step...

    // Create an invocation using the type's method 
    // Signature we want. So, the TPContentView we 
    // Made earlier, and the selector we created above.
    NSInvocation *siriIconInvocation = [NSInvocation invocationWithMethodSignature:[TPKContentView methodSignatureForSelector:siriIconSel]];

    // Set that selector...
    [siriIconInvocation setSelector:siriIconSel];
    // And the target that'll attempt to respond to it...
    [siriIconInvocation setTarget:TPKContentView];

    // Pass the method the trait collection as 
    // An argument, see why it's at index 2
    // After the code sample below.
    [siriIconInvocation setArgument:&traits atIndex:2];

    // Perform it, watch magic happen or 
    // Your app explode...
    [siriIconInvocation invoke];

    // And assign the result to a variable.
    [siriIconInvocation getReturnValue:&returnValSiri];
    
    // And another for the tips
    NSInvocation *tipsIconInvocation = [NSInvocation invocationWithMethodSignature:[TPKContentView methodSignatureForSelector:tipsIconSel]];
    [tipsIconInvocation setSelector:tipsIconSel];
    [tipsIconInvocation setTarget:TPKContentView];
    [tipsIconInvocation setArgument:&traits atIndex:2];
    [tipsIconInvocation invoke];
    [tipsIconInvocation getReturnValue:&returnValTips];
}

As mentioned in the comments on the invocations, one thing to note is that the first two arguments are already set for you. The first, at index 0, is self while the other, _cmd, is at index 1. That’s why we started passing our argument, the trait collection, at index 2:

[siriIconInvocation setArgument:&traits atIndex:2];

// and

[tipsIconInvocation setArgument:&traits atIndex:2];]

Step Five - Run on your Device

This part is often missed, and I forget it sometimes too because I have a million kids and can’t remember if I even had breakfast this morning.

You need to run this on an actual device.

The frameworks you’re after are present on the real thing, so a simulator won’t work here. That said, we’ve got all the code in place and running it on device yields the results!

Here’s the Siri icon:

A Siri Icon

And the Tips icon:

The Tips Icon

Bonus - Why I Prefer NSInvocation

If all you wanted out of this article was how to call private APIs, you can check the door now. Though, if you’re curious as to why I prefer to use the methods outlined here, it boils down to flexibility.

If you use performSelector, it’s easier at the call site but you’re limited to two arguments. Plus, its return type is an NSObject, so if you’re returning primitives then you’re out of luck.

Further, if you go the methodForSelector: route, it’s on you to come up with the definitions of the selector and method types upfront. It’s a little ugly, and this is coming from someone who held on to writing Objective-C with a loving grasp - letting it go for Swift only a few short years ago.

So, for my money, the best way to call private APIs in Cocoa Touch is simply to lace em’ up and roll with Objective-C and NSInvocation. It gives me the right balance of ease and flexibility while limiting the jank. At least, as much as one can avoid jank when diving into private API.

Final Thoughts

While, no doubt, you should exercise tact when applying private API - I would submit to you that you should be far from scared of it. Dig in and explore. You’re bound to learn something by playing around with it. Who knows, maybe you’ll find a solution to a problem or get inspired to look at things from another angle. Sometimes the wrong train will get you to the right station, ya know?

Until next time ✌️

  1. We’re also using UIKit here. 

···

Spot an issue, anything to add?

Reach Out.