Tricking iOS Into Animating App Icons
This is what the GitHub app icon looks like on my normal, unjailbroken iPhone:
That’s… definitely not supposed to be possible. But it is! In fact I have quite a few apps set up like this now.
If you know your iOS APIs well, you might already be able to take a guess at what’s happening here — this is abusing the Alternate App Icons system that lets you offer users the option to pick from a set of pre-determined, static images.
If you change an image frequently enough, you can create the illusion of a moving image — no surprises there — but if you’ve used this API before, you’ll know that there’s more to it than that.
Video version of this post:
Table of Contents
The Problems
For one, when you call setAlternateIconName
, iOS will show an alert to the user:
And not only that, but if you call this method while your app is in the background, nothing happens!
We can easily confirm this with a small sample app. Let’s start with an app where we want to switch between these two images in the background:
First, we’d update our Info.plist
file to include information about the icons —
both our primary icon (named "RedDancer"
) and our alternate icon (named "BlueDancer"
) :
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array><string>RedDancer</string></array>
</dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>BlueDancer</key>
<dict>
<key>CFBundleIconFiles</key>
<array><string>BlueDancer</string></array>
</dict>
</dict>
</dict>
Now we can add some code to switch to our alternate app icon when a button is tapped:
func buttonTapped() {
// Request the app icon change
UIApplication.shared
.setAlternateIconName("BlueDancer") { error in
// Print an error if there is one,
// Otherwise print a success message
if let error = error {
print(error)
return
}
print("App Icon Changed")
}
}
And this works as we would expect:
Now let’s change things around a bit — we’ll use the same code to change our app icon, but instead we’ll run it after a short delay. We’ll also set up a basic background task to ensure that we can keep running code after the app moves to the background.
func buttonTapped() {
// Start a background task to ensure
// our app is not suspended too quickly
backgroundTask = UIApplication.shared
.beginBackgroundTask()
// Schedule the icon change
// for 5 seconds from now
DispatchQueue.main.asyncAfter(
deadline: .now () + .seconds(5)) {
self.changeAppIcon()
}
}
func changeAppIcon() {
// Request the app icon change
UIApplication.shared
.setAlternateIconName("BlueDancer") { error in
// Print an error if there is one,
// Otherwise print a success message
if let error = error {
print(error)
return
}
print("App Icon Changed")
}
// Mark our background task as complete
UIApplication.shared
.endBackgroundTask(backgroundTask!)
backgroundTask = nil
}
This still works when the app is kept open — but if we dismiss the app to the background before our delay ends, then the app icon does not change, and we see an error printed to the console instead:
Error Domain=NSCocoaErrorDomain Code=3072
"The operation was cancelled."
So we have an API that can change the app icon, and maybe if we can call it quickly enough we can create some cool effects on the lock screen — but we have two problems preventing that from being possible.
How do we go about fixing them?
Fixing Problem #1: Alerts
There are a few well-known ways to change your app icon without showing an alert to the user
by using private APIs;
let’s see if we can figure out how they work by taking a look at
what the setAlternateIconName
method actually does.
We can use LLDB to figure out what framework actually implements this method:
(lldb) image lookup -n "-[UIApplication setAlternateIconName:completionHandler:]"
1 match found in /Users/brycebostwick/Library/Developer/Xcode/iOS DeviceSupport/iPhone15,2 17.5 (21F79)/Symbols/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
Address: UIKitCore[0x000000018b342a1c] (UIKitCore.__TEXT.__text + 15550824)
Summary: UIKitCore`-[UIApplication(UIAlternateApplicationIcons) setAlternateIconName:completionHandler:]
Looks like this method lives in UIKitCore.framework
. We can open UIKitCore
in a decompiler
and ask it to decompile this method. Here’s a slightly-cleaned-up version (mainly removing memory management
calls) :
void __fastcall -[UIApplication(UIAlternateApplicationIcons) setAlternateIconName:completionHandler:](
void *a1,
__int64 a2,
__int64 a3,
void *a4)
{
// (1)
v7[0] = (__int64)_NSConcreteStackBlock;
v7[1] = 3221225472LL;
v7[2] = (__int64)__85__UIApplication_UIAlternateApplicationIcons__setAlternateIconName_completionHandler___block_invoke;
v7[3] = (__int64)&unk_1DB0C6EB0;
// (2)
objc_msgSend(a1, "_setAlternateIconName:completionHandler:", a3, v7);
}
From this decompilation, we can see that this method essentially only does two things:
Creates a new Objective-C block on the stack
Calls
[self _setAlternateIconName:completionHandler:]
, and passes in the block it created above.
This is a fairly normal pattern that you’ve probably seen before — this method wants to perform some additional work before the caller’s given completion block is called, so it creates its own completion block that wraps the original one, then calls the method that does the real work.
We can imagine the original implementation looked something like this:
func setAlternateIconName(_ name: String,
completion: (Error?) -> ()) {
// Create our own completion closure
// that can do some additional work
let newCompletion: (Error?) -> () = { error in
// Do our additional work
print("Doing extra things!")
// Call the original completion block
// that was passed in
completion(error)
}
// Call the _real_ implementation
// using our new completion block
_setAlternateIconName(
name,
completion: newCompletion
)
}
Now we can figure out what that newCompletion
block is actually doing.
From the decompilation, we can see the implementation of that block —
where the instructions like the print
statement in the above example would live —
is defined elsewhere, in a function with this impressive name:
_UIApplication_UIAlternateApplicationIcons__setAlternateIconName_completionHandler___block_invoke
(that’s our current function’s name, plus __block_invoke
)
We can then click into that in our decompiler and see what this new block does.
The implementation is too long to include in full, but we can scan through and see some interesting details:
v12 = (void *)objc_opt_new_11(&OBJC_CLASS____UIAlternateApplicationIconsAlertContentViewController);
v13 = _UINSLocalizedStringWithDefaultValue(
CFSTR("ALTERNATE_APP_ICONS_CONFIRMATION_MESSAGE"),
CFSTR("You have changed the icon for “%@”."));
v14 = (void *)objc_claimAutoreleasedReturnValue_6(v13);
v15 = (void *)objc_claimAutoreleasedReturnValue_6(objc_msgSend(off_1D8300F40, "localizedStringWithFormat:", v14, v11));
objc_msgSend(v12, "setMessageText:", v15);
objc_release(v15);
objc_release(v14);
v16 = (void *)objc_claimAutoreleasedReturnValue_6(
objc_msgSend(
&OBJC_CLASS___UIAlertController,
"alertControllerWithTitle:message:preferredStyle:",
0LL,
0LL,
1LL));
This code is responsible for showing the alert message saying that the icon has changed!
If we look back at our example implementation above, we can see that
we can skip this block entirely by just calling _setAlternateIconName
directly. We can do this fairly easily by defining an Objective-C extension
on UIApplication
to expose this method to Swift:
@interface UIApplication()
- (void)_setAlternateIconName:(nullable NSString *)alternateIconName
completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler;
@end
And then call our method just like before, but this time using the underscored-prefixed method:
UIApplication.shared._setAlternateIconName("BlueDancer") { error in
if let error = error {
print(error)
return
}
print("App Icon Changed")
}
And now our app icon changes without showing an alert!
Again, this is not new knowledge - if you’ve ever had a need to change your app icon without showing an alert, you’ve probably found this trick before.
But this does set us up nicely to solve our second problem:
Fixing Problem #2: Background Usage
Why doesn’t this method work while the app is in the background? Is it a real technical limitation, or something we can work around?
Let’s keep diving into how this method actually works. Now that we know
the real method lives in _setAlternateIconName
, we can look at a decompilation
of that method instead (again, slightly cleaned up) :
void __fastcall -[UIApplication(UIAlternateApplicationIcons) _setAlternateIconName:completionHandler:](
_BYTE *a1,
__int64 a2,
void *a3,
void *a4)
{
// (1)
v9 = LSApplicationProxyForSettingCurrentApplicationIcon();
if ( v9 )
{
// (2)
if ( (a1[205] & 0x40) == 0 && !objc_msgSend(a1, "applicationState") )
{
v12 = /* block, impl at __block_invoke_3 */
// (3)
objc_msgSend(v9, "setAlternateIconName:withResult:", v6, v12);
}
v12 = /* block, impl at __block_invoke_2 */
dispatch_async(_dispatch_main_q, v12);
}
else
{
v12 = /* block, impl at __block_invoke */
dispatch_async(_dispatch_main_q, v12);
}
}
This decompilation is slightly more complex, but it’s not too bad either:
We call a function to get an
LSApplicationProxy
— more on that below. If that call returnsnil
, we fall back to calling a block, which presumably invokes our original callback with an error.We check two conditions on
self
(we’re looking ata1
, which is the first argument of this function; for Objective-C methods, the first argument is alwaysself
). Based on the result of those conditions, we either callsetAlternateIconName
on our Application Proxy, or fall back to another block (presumably another error block).If the two conditions pass, then we call
setAlternateIconName:withResult:
on the application proxy we got in step #1
The type of object we get in step #1, an LSApplicationProxy
, is an object
that lets us communicate with other parts of iOS via XPC (you might recognize
it from other private APIs like
listing installed applications).
We don’t have to care about the details of that much right now; the important thing
to recognize is that this is the object that we actually need to talk to in order to
change our icon.
The conditions before that call are a bit more interesting. The first one
(a1[205] & 0x40) == 0
) looks a little opaque at first,
but looks a lot like the output you’d get if you were checking a specific bit in an ivar, like:
if (self->applicationFlags & SomeFlag)
That might end up being interesting, but the second check jumps out way more — we’re calling
if (self.applicationState == 0)
UIApplication.applicationState
is a public property,
so we can actually look up its type and see what the value 0
corresponds to — which ends up being UIApplicationStateActive
.
So this second check is making sure the app is active before continuing!
At this point, we know that we definitely will want to patch out this check,
but we don’t know if the first condition will also be an issue, nor whether the call
to -[LSApplicationProxy setAlternateIconName:withResult:]
even works in the background —
this check might exist because that call fails when the app isn’t active.
To determine if that’s the case, we can quickly patch out this check with the debugger.
One easy way might be to set a breakpoint on -[UIApplication applicationState]
and use the debugger command thread return 0
to make that method always
behave like it’s returning UIApplicationStateActive
:
(lldb) b -[UIApplication applicationState]
Breakpoint 1:
where = UIKitCore`-[UIApplication applicationState],
address = 0x000000018d335004
(lldb) br com add
Enter your debugger command(s). Type 'DONE' to end.
> thread return 0
> con
> DONE
We use br com add
(short for breakpoint command add
) to specify what
commands we want to execute when this breakpoint is hit — then we add
one command to provide our return value, and another command to automatically
continue execution.
This is a valid option, but I’d be worried that it might have larger effects — applicationState
seems like
too important of a thing to widely mess with.
Instead, let’s use a similar system that can target this one specific check only.
Let’s first ask lldb
to provide a disassembled version of this function:
(lldb) dis -n "-[UIApplication(UIAlternateApplicationIcons) _setAlternateIconName:completionHandler:]"
If we scan through the output, we can see the assembly corresponding to the method call we want to patch out,
thanks to lldb
identifying it as a call o objc_msgSend$applicationState
:
0x18e0a6650 <+80>: tbnz w8, #0x6, 0x18e0a6660
0x18e0a6654 <+84>: mov x0, x22
0x18e0a6658 <+88>: bl 0x18ebe96c0 ; objc_msgSend$applicationState
0x18e0a665c <+92>: cbz x0, 0x18e0a6748
0x18e0a6660 <+96>: add x8, sp, #0x30`
The third instruction above calls [self applicationState]
, the result of which will
be placed into register x0
.
The next instruction, cbz
(Compare and Branch on Zero
) checks if
x0 == 0
and branches to a specific address
if it is. This matches the behavior we saw in the decompilation above.
To modify this behavior at runtime, we can set a breakpoint on the address of that fourth instruction:
(lldb) b -a 0x18e0a665c
Breakpoint 1:
where = UIKitCore`-[UIApplication(UIAlternateApplicationIcons) _setAlternateIconName:completionHandler:] + 92,
address = 0x000000018e0a665c
This breakpoint will be evaluated before this cbz
instruction is run.
Since the instruction is just checking if x0 == 0
, we can force
it to always take the same path by setting x0
ourselves before
the instruction is executed — effectively overwriting the return value of
the [self applicationState]
call from the previous instruction.
(lldb) br com add
Enter your debugger command(s). Type 'DONE' to end.
> register write x0 0
> con
> DONE
With this breakpoint set up, we can resume execution, schedule an app icon change, move our app to the background, and…
Success! We can silently change app icons from the background.
Now we just need to patch out this call in a more permanent way.
Calling LSApplicationProxyForSettingCurrentApplicationIcon
would let us skip this check,
but calling a private C function like this can be annoying.
Luckily, we have a better option — looking at the decompilation of
that function, it essentially ends up
being a call to +[LSBundleProxy bundleProxyForCurrentProcess]
— a private Objective-C
method, which will be much easier for us to call.
We can define interfaces for both LSApplicationProxy
(to actually change
our icon) and LSBundleProxy
(to access that LSApplicationProxy
) :
@interface LSApplicationProxy
-(void)setAlternateIconName:(NSString *)name
withResult:(void (^)(NSError *))result;
@end
@interface LSBundleProxy
+ (nonnull LSApplicationProxy *)bundleProxyForCurrentProcess;
@end
Now back in Swift, we can attempt to change our icon like so,
this time passing nil
to reset our icon:
let appProxy = LSBundleProxy.bundleProxyForCurrentProcess()
appProxy.setAlternateIconName(nil) { error in
if let error = error {
print(error)
return
}
print("App Icon Changed")
}
However, we run into an issue; despite our ‘success’ print statement being called, our app icon doesn’t actually change, and we see a new error appear in the console:
LaunchServices: disconnect event interruption
received for service com.apple.lsd.icons
App Icon Changed
Remember that LSApplicationProxy
is an object designed to communicate
with another part of iOS over XPC — at some point during that communication,
the parameters we pass to this method will need to be serialized so they can
be sent to another process, and XPC generally has some pretty strict expectations about
what is passed in accordingly (compared to in-process Obj-C method
calls, which can be a lot looser in their typing). In my experience,
errors like this are often due to typing issues, especially with blocks.
Let’s look more closely at the block we’re passing in. We can get more information about this block by setting a breakpoint on the method we’re calling:
(lldb) b -[LSApplicationProxy setAlternateIconName:withResult:]
Then when this breakpoint is hit, we can print out a description of the block.
It’s the fourth parameter to the method (the first is the implicit self
that’s
part of every Obj-C call; the second is implicit _cmd
; the third is the alternate
icon name; and the fourth is the block). That fourth parameter should be stored
in register x3
:
(lldb) po $x3
<__NSMallocBlock__: 0x303348030>
signature: "v16@?0@"NSError"8"
invoke : 0x1009d962c
... >
The signature
data is especially helpful — this is an
Objective-C Type Encoding
that describes the exact format of our block:
v
— this block’s return type is void16
- we have 16 bytes worth of arguments@?0
- our first parameter is an object (@
) of an unknown type (?
) at byte index 0@"NSError"8
- our second parameter is an object (@
) of typeNSError
at byte index 8
This lines up with what we’d expect — the first parameter to every Objective-C block is an opaque type
containing metadata about the block itself. Then we have our NSError
pointer, and that’s it.
Let’s compare this to the block format that we should be using — we can find this by using the
same breakpoint as before, but this time triggering it by calling -[UIApplication setAlternateIconName]
,
the original public API we were using. This will internally call -[LSApplicationProxy setAlternateIconName:withResult:]
(as we learned earlier), at which point we can see what block was passed in:
(lldb) po $x3
<__NSStackBlock__: 0x16d82cc18>
signature: "v20@?0B8@"NSError"12"
invoke : 0x18e0a689c
... >
This block has a different encoding! Let’s break it down again:
v
— this block’s return type is void, same as before20
- we have 20 bytes worth of arguments — not 16@?0
- our first parameter is an object (@
) of an unknown type (?
) at byte index 0, same as beforeB8
- our second parameter is aBOOL
(B
) at index 8@"NSError"12
- our last parameter is an object (@
) of typeNSError
at byte index 12
Our block is missing a parameter! There’s a BOOL
param that we didn’t know about — presumably
indicating whether or not the operation was successful.
We can update our header with this new information:
@interface LSApplicationProxy
-(void)setAlternateIconName:( NSString *)name
withResult:(void (^)(BOOL success, NSError *))result;
@end
And update our callsite to match, again passing nil
to reset
our icon:
let appProxy = LSBundleProxy.bundleProxyForCurrentProcess()
appProxy.setAlternateIconName(nil) { success, error in
if !success || error != nil {
print("Error: \(error as Any)")
return
}
print("App Icon Changed")
}
and…
It works! We can now change our app icon while our app is in the background, without a debugger attached.
Animated Icons
Now we have everything we need for our final task: seeing how fast we can call this thing in a loop!
We’ll leave behind our lovely disco icons and switch to something a little more sensible for an animation:
I’ve gone ahead and generated 30 variants of this icon, with each subsequent
beach ball being slightly further rotated than before. I’ve also updated the CFBundleAlternateIcons
entry in our Info.plist
file to list all of these icons:
<key>CFBundleAlternateIcons</key>
<dict>
<key>BeachBall000</key>
<dict>
<key>CFBundleIconFiles</key>
<array><string>BeachBall000</string></array>
</dict>
<key>BeachBall001</key>
<dict>
<key>CFBundleIconFiles</key>
<array><string>BeachBall001</string></array>
</dict>
<!-- ... -->
</dict>
Now we need some mechanism that can run every frame to let us update our icon.
This would normally be a good place for a
CADisplayLink
, but those don’t
actually run in the background.
A Timer/NSTimer
gets us most of the way
there, but I haven’t had good luck with them when targeting frame-level precision; their documentation
even specifies that
the system may apply a bit of tolerance as needed.
Instead, we can drop down to
CFRunLoopTimer
,
which I’ve had better luck with here — and
for which the documentation makes some slightly stronger promises:
The fine precision (sub-millisecond at most) of the interval may be adjusted slightly by the timer if implementation reasons to do so exist.
We can set up the timer to try to run 30 times per second, and just increment the frame index that we’re showing each time the timer fires; but that will result in the animation pausing and lagging if the timer isn’t able to run perfectly every frame (like if the main thread is blocked).
Instead, we can mark down the start time of our animation, and every time the timer fires, use the difference between the start time and the current time to determine what frame we should be on.
The timer setup looks something like this:
func startAnimation() {
// Mark the start time of our animation
animationStartTime = CACurrentMediaTime()
// Calculate the duration of each frame, in seconds
let frameDuration = 1.0 / 30.0
// Calculate the time when we should start our timer.
// This is set to be halfway through the next frame
// interval, so that when we try to determine what
// frame we're on, we're not near a boundary
// between frames
let startTime = animationStartTime + (frameDuration / 2)
// Create our timer
let timer = CFRunLoopTimerCreateWithHandler(
/* allocator: */ kCFAllocatorDefault,
/* fireDate: */ startTime,
/* interval: */ frameDuration,
/* flags: */ 0,
/* order */ 0
) { [weak self] timer in
self?.updateFrame()
}
// Actually start the timer
CFRunLoopAddTimer(
CFRunLoopGetMain(),
timer,
.commonModes
)
}
And the logic for updating the frame then becomes:
private func updateFrame() -> Bool {
// Compute the time since the start
// of the animation
let timeSinceStart
= CACurrentMediaTime() - animationStartTime
// Determine what frame we should be on
// based on how long the animation has
// been running
let currentFrame =
= Int(timeSinceStart * 30.0) % numberOfFrames
// Get the name of the icon to use
let iconName = String(
format: "BeachBall%03d",
currentFrame
);
// Actually update our app icon
appProxy.setAlternateIconName(iconName)
{ success, error in
if !success || error != nil {
print("Error: \(error as Any)")
return
}
}
}
Now when startAnimation
is called — whether in response to user input,
or to the app heading into the background, or due to a remote notification:
We have a working animation! There’s a little work to do to clean up this implementation — I’ve done a lot more in the demo project on that front — but we’ve shown that the core logic works!
In this example, we’re running at 30 FPS, but I have had some luck pushing this all the way up to 60 FPS on a newer phone, especially after making sure the icon files are no larger than they need to be (180x180), compressing them using ImageOptim, and running in a release build without a debugger attached.
The largest source of lag I’ve seen is from situations where the timer doesn’t fire for a frame or two, likely from the main thread being occupied with something else, even on an otherwise-empty sample app. I’ve been able to work around that by handling the loop on the main thread rather than on a timer; you can see that as an option within the demo app’s source. That option doesn’t make sense for most apps, but then again — none of this makes sense for most apps!
Wrapping Up
Again, a demo project for this animation is available on GitHub. Like most things on this site, this uses private APIs that would not be allowed on the app store — but if you end up using something like this in your own personal app, let me know!
By the way, my absolute favorite animated icon I’ve implemented so far is for YouTube, whenever the new bryce.co account gets a subscriber:
Consider checking it out if you haven’t already — there are videos for each of these blog posts, plus more!
Say Hello!