Tricking iOS Into Animating App Icons


Bryce Bostwick

 •  •  • 

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:

An alert showing that the app icon was successfully changed An alert showing that the app icon was successfully changed

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:

Red app icon with a dancer facing right Blue app icon with a dancer facing left

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:

An alert showing that the app icon was successfully changed An alert showing that the app icon was successfully changed

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 UIKitCorein 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:

  1. Creates a new Objective-C block on the stack

  2. 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:

  1. We call a function to get an LSApplicationProxy — more on that below. If that call returns nil, we fall back to calling a block, which presumably invokes our original callback with an error.

  2. We check two conditions on self (we’re looking at a1, which is the first argument of this function; for Objective-C methods, the first argument is always self). Based on the result of those conditions, we either call setAlternateIconName on our Application Proxy, or fall back to another block (presumably another error block).

  3. 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:

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:

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:

macOS Beach Ball Loading Indicator on an iOS icon

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!