LongCut logo

Apple’s Widget Backdoor

By Bryce Bostwick

Summary

Topics Covered

  • Apple's Widget Backdoors Enable Developer Animations
  • Timer Trick Animates Widgets at 1 FPS
  • Private Clock API Revived via Xcode Workaround
  • Nested Clocks Trace Arbitrary Paths
  • Timer Ligatures Enable 30 FPS Without Private APIs

Full Transcript

This clock should not be possible to make. Or this one. Or this one, which I

make. Or this one. Or this one, which I think is the single most advanced clock to ever appear on an iPhone. Uh Bryce,

that last one's not a clock. You're not

going to believe this. That is a clock.

Apple left a back door in their widget system. When they were adding support

system. When they were adding support for widgets on iOS, they decided it shouldn't be possible to make animated ones like this. And yet, in typical Apple fashion, they added an exception for themselves. They decided, even

for themselves. They decided, even though we don't want to allow developers to make smoothly animated clocks like this, we should be allowed to make that clock, right? So, they left in a back

clock, right? So, they left in a back door, a private API that can be used to make this clock or this clock or this one. Just because it animates doesn't

one. Just because it animates doesn't mean it's a clock. No, I'm telling you that is a clock. And this is normal for Apple. They add exceptions for

Apple. They add exceptions for themselves all the time. Like when I made a video about a complicated workaround to make animated app icons, people rightfully in the comments asked, "Well, how does Apple do it for their clock app, their calendar app?" They're

Apple. They just cheat. Like here, if you open up the framework that's responsible for displaying the home screen, here's the method that determines what class should power the icon for any given app. And if I bring up the decompilation for this method, we can see the home screen literally

checks, is this the clock app? If so,

use this special icon, is this the calendar app, then use this special icon. Apple does not have to compete on

icon. Apple does not have to compete on a level playing field, as several lawsuits are starting to point out. But

this clock widget is extra interesting because unlike most of Apple's other backd doors into special iOS functionality, where they add entitlement checks to make sure only they can use them, they left this one wide open and some apps are starting to

take advantage. In fact, quite a few

take advantage. In fact, quite a few apps. Now, we're going to have to jump

apps. Now, we're going to have to jump through quite a few hoops to figure out how these animations work, but let's take a step back for a second because you actually might have seen some animations on widgets before. As of iOS 17, Apple actually provides a very barebones API for doing some kinds of

animations, but that's not what I'm talking about. This one only works when

talking about. This one only works when iOS pulls new data into your widget, and it's very limited. It's only for super basic transitional animations and is limited to a duration of 2 seconds.

That's not what I'm talking about. I'm

instead talking about a trick that people have been using for years to make animations work on widgets, actually using public APIs. It's based around this, a label showing a timer. That's

right, a different clock. See, the

tricky thing about widgets in general is that you're not directly in control of what's happening in this little box.

This widget does not directly execute your code. Instead, iOS at some

your code. Instead, iOS at some indeterminant time asks your app, "Hey, what do you want to show in your widget?" And your app responds with

widget?" And your app responds with whatever layout you want to put on screen. You can either send a single

screen. You can either send a single layout or multiple layouts with instructions on at what time you want to show each one. And then those layouts are serialized and passed over to the widget process, which will deserialize them and display the appropriate one based on the current time of day. And

then when time progresses and new content should be shown, the widget process automatically knows to switch over to the next layout with no need to talk to your app again. This is very different than the normal app execution model where you have a long running process that's fully in control of

what's currently on the screen. Instead,

your code is running in a separate process that only goes for a couple seconds, maybe a few times a day, and it can only use things that are explicitly supported by this serialization process.

So even though it's easy to write normal Swift UI code that will make a view rotate, we can make a rotation variable to store the current rotation. We say

our view's rotation should match that state and then when that view appears we start animating that state and like that's it view rotating. But if we add a widget to this app with this same code and a widget extension this rotation

doesn't happen or more accurately it happens instantaneously. We can see if

happens instantaneously. We can see if we set this to an incomplete rotation that we now get an Australian hello world on the screen because widgets just don't support this kind of animation.

And some of the tricks you might be thinking of here to emulate an animation also don't work like making content changes super quickly. Apple only allows us to provide updates that are 5 minutes apart at minimum. Well, I guess that's technically an animation, but we want to

do better here. What all this means is that if you're trying to do something fun with widgets, you have to do so within the constructs that Apple provides to you. And that brings us back to timer. One of those Apple provided

to timer. One of those Apple provided constructs is a label that acts as a timer that counts up or down from some arbitrary reference point. This is

obviously an interesting component because it changes over time. iOS is

still only asking us for our widget layout once, but when we send a timer over, the operating system knows to automatically update it every second without any intervention from us, i.e.

an animation. Great. It's not the most exciting animation, but you can actually start to do some cool things with this.

For one, you get to change the style of the label. You can set its size or its

the label. You can set its size or its color or its font like this one or this one or this one. Now, we're getting somewhere and we can start to see how the built-in stuff can be abused a little. This is a custom font where

little. This is a custom font where instead of the numbers 0 through 9ine looking like this, they look like this.

Now, let's update the font to also replace zero and also leave an empty space instead of a colon. So, we have a nice monospace font. Now, because this is actually a timer, we can see our multiple different digits here and the space for the colon. This looks like nonsense. So, let's clean it up just a

nonsense. So, let's clean it up just a little bit. I'm going to take all this

little bit. I'm going to take all this and I'm going to wrap it in a container.

And let's set a background on that container so we can see the space we're working with. Now, we're going to start

working with. Now, we're going to start by giving this timer a consistent frame.

And we should make sure we have one that's wide enough to show a bunch of digits in case this overflows to multiple hours. So let's say we should

multiple hours. So let's say we should have enough room to show nine digits.

Now we want to make sure the ones digit is in a consistent spot because right now once the timer hits 10 minutes, everything's going to be shifted over, right, to make room for a new digit. So

let's give the overall label a trailing alignment. That way we know the on's

alignment. That way we know the on's place is always going to be at the very end. And now I want to shift the entire

end. And now I want to shift the entire label so that that ones place is actually in the middle. In this case, that means I'm going to shift it four digits to the left. Cool. Now our ones place is perfectly centered. All that's

left to do is to make sure our overall container is just wide enough to show that single digit. And we're also going to go ahead and clip out any content that's outside of that frame. And now

there we go. We have a single animated sprite. We can get rid of our background

sprite. We can get rid of our background color. We can scale it up if we want. We

color. We can scale it up if we want. We

can shift it around and put it anywhere on the screen we'd like. Whatever. We

just have this like a normal view that we can move around. Neat. iOS is still only running our code once to determine what we want to put on screen. We're not

rerunning our code every second.

Instead, we found a way to return a single layout whose appearance changes every second. thanks to that timer API.

every second. thanks to that timer API.

This is one of the first ways people came up with to animate widgets, and most animations you're familiar with are probably using some variants of this setup. One or multiple timers set with

setup. One or multiple timers set with custom fonts. There's a pretty big

custom fonts. There's a pretty big difference between these two clocks, though. This one runs at one frame per

though. This one runs at one frame per second, but this one runs at a buttery smooth 20 fps. 20 FPS. We'll take what we can get. So, how does that one work?

Well, we know this functionality is obviously used by Apple's own clock widget. So, let's take a look at its

widget. So, let's take a look at its implementation. We could do that

implementation. We could do that somewhat easily with a jailbroken phone, but if you don't have one lying around, you can also get access to this information from an IPSW file, the file type that iPhones use for firmware updates. This is a lot easier with

updates. This is a lot easier with Blacktop's IPSW tool. It's just one of the most useful iOS tools out there.

It's great. So, I'm going start by asking it to download the firmware for my own device model, even though it doesn't matter a ton here. And let's

just go ahead and grab the latest version. Now, we just have to wait a

version. Now, we just have to wait a little bit. Now that that's done, I'm going to

bit. Now that that's done, I'm going to ask IPSW to mount the file system from that firmware update. And if I list the contents of this directory, you'll see the same root file structure that we would see if we sagged into a jailbroken phone. If you're used to the file system

phone. If you're used to the file system on a Mac, you'll recognize it as a very similar system. So somewhere in this

similar system. So somewhere in this directory should be the clock app since that comes bundled with the phone. And

somewhere in that app should be the clock widget. But we have to find that

clock widget. But we have to find that app first. I usually like taking the

app first. I usually like taking the easiest initial approach here, even if the chances of success are low. So I

might start by just searching for clock.app, which I assume would be a

clock.app, which I assume would be a pretty reasonable name for Apple to name this, but no luck. So let's try something a little more robust. If I go to the add widget screen on my phone and then I search to add a clock widget, there are a bunch of pieces of text on

the screen that presumably live within the clock app in its widget implementation specifically. So, I'm

implementation specifically. So, I'm just going to take the subtitle here, the one that says display the current time. And I'm going to go ahead and GP

time. And I'm going to go ahead and GP for that within this file system. And

I'm using the binary flag to indicate we also want to search in binary files. And

here we go. We got two results. They

both live in an app called mobile timer, which I personally think is a worse name than clock, but to each their own. And

they specifically live within a widget app extension. So, perfect. This seems

app extension. So, perfect. This seems

like where the widget actually lives.

Now, we want to poke around at this widget in a disassembler. So, I'm going to go ahead and open this directory and finder. And now we can right click on

finder. And now we can right click on the extension, go into the package contents, and if we scroll down a bit, we'll find the main binary for this widget. So, let's go ahead and drag this

widget. So, let's go ahead and drag this into our disassembler. And all the default options here should be okay for our case. Now, widgets are implemented

our case. Now, widgets are implemented in Swift UI, and Swift UI is awful to disassemble. Like, I would not wish it

disassemble. Like, I would not wish it on my worst enemy. But luckily, we shouldn't have to dig too far here because the thought is that this widget is using a private API somewhere. and

the disassembler will actually show us all the different symbols we're importing from different frameworks. So

if this clock widget is using some private API, we should actually be able to see it here. That's not always true for Objective C where things are dynamically dispatched, but for Swift, we should have pretty good luck. So if

we scroll down and look at the symbols that this widget is referencing from the widget kit framework, we see a very interesting one, underscore clock hand rotation effect. This sounds like it's

rotation effect. This sounds like it's probably the thing making clock hands rotate. If we click into it, we can see

rotate. If we click into it, we can see that it takes a few parameters. The

first one is a period, presumably indicating how fast the hand turns. The

next is a time zone, presumably to keep the hand automatically at the correct offset, and then an anchor point that we rotate around. Now, that's how you might

rotate around. Now, that's how you might find this symbol organically if you were trying to reverse engineer this for yourself. But that's not how people

yourself. But that's not how people first stumbled onto this because Apple made a mistake. Let's take a look at an old copy of Xcode. I'm using version 13 from a few years back. If I try to open this, Mac OS will stop me, saying that

it is simply too old. You expect me to open software from 3 years ago? I'd

rather die. You can sometimes bypass by trying to open the actual executable file within this app. And in our case, that actually does get us part of the way there. But then if I try to open a

way there. But then if I try to open a new project, Xcode crashes. So we're

going to have to open it in a virtual machine running an old copy of Mac OS instead. Let's throw together a widget

instead. Let's throw together a widget app real quick. I'm going to start a new project. And we're just going to use the

project. And we're just going to use the normal app template. And then just like before, I'm going to add a new target and create a widget extension. Okay. And

this adds all sorts of boiler plate for us. But importantly, here's where the

us. But importantly, here's where the actual widget lives. And let's go ahead and run this on a simulator. And there

we go. We have the default widget template running. Now, here's the thing.

template running. Now, here's the thing.

If I go to add a modifier to this view and I start typing a little bit, we'll see that clock hand rotation effect is actually visible to Xcode and if I go ahead and try to use it, let's say we want a custom period of 5 seconds. We

want it in the current time zone and just the center point is fine. If I

rebuild, we have a rotating widget that is not supposed to happen. This is

clearly supposed to be a private API.

But for some reason in Xcode 12 and 13, this was actually a publicly accessible symbol. Even though it's still marked

symbol. Even though it's still marked with an underscore, it's not documented.

you're clearly not supposed to use it.

It was still possible to use. Now, don't

get me wrong. Even though this was accidentally public, this modifier still is not common knowledge. If you look at the results searching for this modifier name, you'll see that it's limited to three pages on Google, which is a lot compared to some other topics we've

looked at, but this is not a widely known trick. Part of the reason for that

known trick. Part of the reason for that was that this was only public for a year or two. With Xcode 14, Apple fixed this

or two. With Xcode 14, Apple fixed this issue, which is why we can't directly use this modifier anymore. But at least a couple folks came up with a clever workaround. See, the API is still there.

workaround. See, the API is still there.

As we saw by disassembling the clock widget from a new version of iOS, our copy of Xcode just can't see it. But

this old version of Xcode still can. So,

let's close out all this and instead I'm going to make a new project. And this

time, I'm going to make an iOS framework. I'll call it clock hand

framework. I'll call it clock hand rotation effect. And now we have an

rotation effect. And now we have an empty Swift framework to work with.

First, I'm going to create a new view modifier with a very similar name to the private one. And then in the body of

private one. And then in the body of this view modifier, which determines what this modifier does, I'm going to use it to add the private clock hand rotation effect. And for now, let's just

rotation effect. And for now, let's just hardcode some values here. So, what does this mean? If we take this view modifier

this mean? If we take this view modifier and apply it to a view, it will then turn around and apply this private modifier. Now, we can clean this up a

modifier. Now, we can clean this up a little bit. At minimum, we should offer

little bit. At minimum, we should offer a way to change how fast you want something to spin. So, we'll add an initializer that takes that in. And

then, we'll also add an extension to all views to make this a little bit easier to use where they get a clockand rotation effect function that takes in that time interval where this extension just automatically adds that modifier to a given view. Now, one last thing over

in old Mac OS land. I'm going to actually build this framework. To

actually do this from Xcode is kind of annoying. So, let's do it from the

annoying. So, let's do it from the terminal. The next 5 seconds aren't

terminal. The next 5 seconds aren't super interesting. Just including for

super interesting. Just including for completeness. We're basically building

completeness. We're basically building one copy of this framework for actual devices, one copy for simulators, and then merging the two together. Also, we

got to set build libraries for distribution to yes. Okay. So, now we've successfully created an XC framework.

But why? Well, cuz now we can take that framework and drag it out of our old copy of Mac OS and into our modern-day copy. And then we can go ahead and add

copy. And then we can go ahead and add it to our Xcode. Oh, something's very upset with us. Maybe we should name this something different from the actual framework name. Shout out to the people

framework name. Shout out to the people screaming at their computers that that was a bad idea. Now, if we add that framework to our project and then we go ahead and import it at the top of this file, Xcode still won't be able to see this Apple provided underscore clockand

rotation effect, but it doesn't need to because thanks to that framework, we have this new modifier that internally actually just uses that same API. I'm

going to switch back to a regular font and then let's go ahead and build and run this. And there we go. In a modern

run this. And there we go. In a modern copy of iOS, we are able to still use this clock hand rotation. We have a framework that reexposes that private API. Now, if you're wondering, okay, we

API. Now, if you're wondering, okay, we have a single API that creates a steady rotation. That's cool, but what does it

rotation. That's cool, but what does it have to do with all the complex animations we saw at the start of this video? Don't worry. This is about to get

video? Don't worry. This is about to get very weird very quickly. Now that we can use this API again, let's take a closer look at what options we have available to us. Let's simplify our widget a

to us. Let's simplify our widget a little bit. We're going to have a single

little bit. We're going to have a single rectangle. We'll give it a background

rectangle. We'll give it a background color of red and a fixed width or height. And then let's go ahead and make

height. And then let's go ahead and make that rotate around. We're only exposing a single parameter here, the period of the rotation. There were other ones in

the rotation. There were other ones in the original API, but they didn't appear super useful. So, we can make this as

super useful. So, we can make this as long or short as we want to make this spin faster or slower. We can even make it negative to make the object spin backwards. That's kind of it on its own.

backwards. That's kind of it on its own.

That's our only parameter. But we can also combine this with other modifiers.

We can scale this view up or we can offset this view before adding the rotation. Meaning we can rotate around a

rotation. Meaning we can rotate around a different point than just the view center. And a question you may have been

center. And a question you may have been wondering already, you can indeed nest these. Meaning we can have this inner

these. Meaning we can have this inner object spin while also rotating on a larger axis. Now, if you're a certain

larger axis. Now, if you're a certain type of geek, you might get immediately excited by this concept. You may have seen nested circles like this as a way to represent a 4A series where you take nested rotating circles, each with their own position, size, and speed, and use

that to draw any path. If you're

interested in the math behind this, there's a three blue, one brown video that goes way into depth here. But it's

not super hard to actually construct a basic version of this by hand. If we

want to animate something along this path, then all we need is two circles.

One larger one centered on that path itself, and then one smaller one rotating within that outer one's radius going in the opposite direction. With

those two circles combined, we end up with this point traveling linearly along this path. And we can construct that

this path. And we can construct that using clock hand rotation effect. Let's

get rid of all this and start with just a single circle. I'm going to color it just off-white and give it a set frame size. Now we've got our outer circle.

size. Now we've got our outer circle.

Let's stack our inner circle on top of that. So I'm going to set the size of

that. So I'm going to set the size of this overall container to be 200 by 200.

And we're going to have this smaller circle within. If I try to build this,

circle within. If I try to build this, we'll just have two nested circles. So

I'm going to update our container to say that inner circle should be aligned to the bottom. Perfect. So there's our two

the bottom. Perfect. So there's our two circles, but we need something to actually move along this path. So I'm

going to again take that inner circle and replace it with another Zstack. And

this time I'm going to add a tiny red circle that we can use to trace our path. And we'll say that inner circle

path. And we'll say that inner circle should be top aligned. And I'm actually going to manually bump it up by just a couple pixels so that it's perfectly centered. I know we've hard-coded a lot

centered. I know we've hard-coded a lot of size and position information so far, but I think it makes the demo easier. So

hopefully it's understand where they all come from. We have a big outer circle.

come from. We have a big outer circle.

We have an inner circle of half its size. And then just a tiny arbitrarily

size. And then just a tiny arbitrarily sized circle inside of that. Now, we

just have to actually rotate all these things. So, I'm going to start by adding

things. So, I'm going to start by adding a clock hand rotation effect to that outer circle. And then, I'm going to add

outer circle. And then, I'm going to add a rotation in the opposite direction to that inner circle. And we're actually going to need to do that at half the speed. Once we do this, we should see

speed. Once we do this, we should see that red dot moving perfectly linearly across the screen here. And there we go.

That's awesome. We can replace them with just empty space. And now it's even easier to see this point just moving back and forth. Now, this only looks good right now because that red circle is a circle. We have one more issue

here, which is that if we try replacing that with an actual view, say a small emoji image, we'll see that that view is actually rotating right now, which makes sense. It's again in a bunch of rotating

sense. It's again in a bunch of rotating circles, but it's rotating at a constant rate. So, we can actually counteract

rate. So, we can actually counteract that using one more clock hand rotation effect. And now you have a perfect smile

effect. And now you have a perfect smile moving across the screen on a fixed path. And again, not only is this just

path. And again, not only is this just powered by a bunch of circles, these are specifically circles that we're telling the widget system to treat as clocks.

Like here, if I replace the circles with kind of a mock clock view, this is what we're looking at right now. And I think that's incredibly hilarious. So now

we've seen how we can take this rotation and convert it into something that moves a view along a path. Now, we said earlier we could make any arbitrary path using nested clocks like this. But to

make an excessively complex path requires an excessively complex number of clocks. That includes movement as

of clocks. That includes movement as simple as a view moving to one side of the screen, pausing for a bit, and then continuing to move on its path backwards. super easy to create using

backwards. super easy to create using normal code. Super hard to create using

normal code. Super hard to create using nested clocks. Given enough of them, you

nested clocks. Given enough of them, you can build something approaching that.

But to get really smooth looking movement there, you would need hundreds of clocks. And my phone taps out

of clocks. And my phone taps out somewhere around 200 to 300. So, this

technique isn't without its limits, but it's still way more powerful than anything Apple provides you out of the box and also incredibly funny in its operation. Luckily, we actually have

operation. Luckily, we actually have some other options. This is probably a good time to talk about Top Widgets.

This is the app we looked at a couple videos ago because it included a surprising number of anti-debugging techniques, including some outright malicious ones. Now, we saw that there

malicious ones. Now, we saw that there were already other types of apps using these widget techniques, but I think Top Widgets might have actually been the first, hence all these protections. But

Top Widgets did something super interesting for an app so secretive and protective. They actually open sourced a

protective. They actually open sourced a repo showing off this technique. This

has been out since like 2003. Why did

they do that? Well, the reason is this is not the most powerful technique you can get by abusing these clocks. Not

even close. they were still keeping the true secret recipe fully locked down.

But people have started to figure out how these other methods work, too, and share them publicly as well. So, it's a good reminder that client side protections can only get you so far.

We're going to take a look at what these apps are currently doing. But don't

worry, after that, we're going to extend it further with a better system that I don't think anyone's actually figured out yet. But in either case, to start

out yet. But in either case, to start with, how do you take a bunch of fixed rate rotating clocks like this and turn it into something like this? Well, this

one requires just one big aha moment.

And it has to do with the fact that a point near the edge of a circle is rotating much faster than a point near its center. You're probably familiar

its center. You're probably familiar with this concept already, but just to say it explicitly, this blue dot only needs to travel a small distance in the time it takes to complete a rotation.

Whereas the red one has to travel a lot further, hence it's moving faster. Now,

instead of drawing a bunch of circles here, let's instead draw a bunch of circular slices. First, I'm going to

circular slices. First, I'm going to define how many slices we want. Let's

just say eight. If we have eight slices, that means that each one is going to take up an eighth of 360°. And then for each of those slices, let's go ahead and draw a shape. And I'm going to give each

one a random color as well. Oh, so if we run this, we see our circle spinning.

Right now, the circle's pretty small, 200 by 200 pixels. Let's make it huge.

So that looks pretty much like we'd expect. Remember, we have giant slices

expect. Remember, we have giant slices here, and we're zoomed in on the middle.

But remember, the points further away from the center are moving faster right now. So what happens if we shift this

now. So what happens if we shift this whole construct down a little bit, let's say a,000 pixels here? We can still get a good sense of what's going on. We can

see those slices moving across the screen. But from this perspective, the

screen. But from this perspective, the whole view is moving a good amount faster. What if we go even further?

faster. What if we go even further?

10,000 pixels shifted instead of just a thousand. Now, on some of these frames,

thousand. Now, on some of these frames, you can see just a hint of movement as the slices still come across, but most of that movement is invisible. Mostly,

it's just a quick switch between different slices. Let's go even further.

different slices. Let's go even further.

100,000 would be right at the edge of the view. So, let's maybe go 90% of the

the view. So, let's maybe go 90% of the way up these slices. At this point, there's zero visible movement. There's

no longer any context of rotation. and

it's just flashes of different colors.

So, okay, how does that help us? Okay,

here's the fun part. Let's get rid of this offset for a second. And I'm

actually going to switch to this showing a single slice. Cool. There's our one slice moving around. Swift UI has a modifier called mask, which lets us take a view and apply a mask to it such that we can only see through that mask. As an

example, if I add our smile emoji back here, and I give it a mask, and I'm just going to remove this entire rotating pie slice into that mask. Now, we have the smile emoji, but we can only see the bit where that slice is currently located.

Now remember, if I undo all this, right now we're looking at a single rotating pie slice close to its center. Let's add

this offset back so that we're actually looking very far away from the center at a rotating point way up the circle. Now,

every few seconds, we see just a flash of that slice. If I add this image back and we give it a mask again, and we put this entire construct within that mask, now we no longer have a blinking slice.

Instead, we have a blinking image or in other words, an animation frame. And

remember, right now we're only creating one slice, but we can create as many as we want. So, let's refactor this a

we want. So, let's refactor this a little bit. Instead of one rotating

little bit. Instead of one rotating collection full of slices, let's actually give each slice its own individual rotation. So, I'm going to

individual rotation. So, I'm going to move all these modifiers in here to modify the slices themselves. Now, if we build this, we should see the same style of view that we saw before. We're

flashing between different colors here.

But now, each of these slices is its own independently rotating view, which means we can create a stack of images here.

Let's try just this first. So, I have a bunch of different image assets, and you can see right now they're all on screen just stacked on top of each other. So,

what we want to do is add a mask to each one where each of those masks is a different slice. Meaning that one frame

different slice. Meaning that one frame will show for a split second, then the next, then the next. And if we try running this, we can see an honest to god animation happening in the widget app right now. This only looks a little bit faster than what we had before, but

critically, we're in control of everything here, including how fast the animation is playing. If I change the period from a five to a one, there we go. And remember, what we're really

go. And remember, what we're really seeing here is eight different clock effects stacked on top of each other where the hands are moving at a rotation of one per second. But we've made the clocks so huge and move so far away from their centers that the hands are just

passing over the screen impossibly fast, showing and hiding each frame in the process. Oh my god, it's just clocks.

process. Oh my god, it's just clocks.

That's what I'm saying. So that's how this style of animation works. We are

blatantly abusing a back door left in by Apple in one of the coolest ways I've ever seen. It's still hard to figure out

ever seen. It's still hard to figure out exactly where the strategy originates from, but if it's from the top widget steps, shout out to you. This is genius.

This also, beautifully enough, combines super nicely with the other animation style we saw earlier, where if I take this whole stack and move it here, we can now have an animation traveling across a path, which is super useful for

any sprite based animations you want to put in a widget. So, you might be thinking, okay, we have the power to render arbitrary frames. That gives us the power to do literally any animation we want, no matter how complex. We can

render an animation to a bunch of static frames and then just show those frame by frame. Unfortunately, it's not that

frame. Unfortunately, it's not that simple. You need one clock per frame

simple. You need one clock per frame using the strategy. And I mentioned before that my phone starts to give out around 200 or so. And you hit stability issues long before that. At 30 frames per second, that means you can only get a few seconds of animation before the

phone starts to get upset. So, it would be impossible to make something like this animation, which says, "Please remember to subscribe if you're enjoying these videos. It helps you find them in

these videos. It helps you find them in the future, but it's also just a huge, huge help to me and the channel. That's

why I genuinely appreciate it a ton. So,

how does that work? I hope you're ready for the big twist of this video. Because

even though this is the state-of-the-art that apps are using to build complex animations, you don't need to use this private clock API at all. This super

complex, super fluid animation is not using any rotating clock hands. It's

using the public timer API. That's

right, it's a throwback to 20 minutes ago. Let's take a look back at our first

ago. Let's take a look back at our first timerbased example where we have a steady one frame per second animation.

Let's see how much further we can push this. First of all, it might look like

this. First of all, it might look like we're limited to just 10 unique frames here because this is a font where we've replaced the digits 0 through 9, but we actually have a lot more options here thanks to font liatures. You may have seen these before. This is a way for

fonts to create merged representations of certain characters like the two glyphs for F and I merging into one shared glyph. You may have also seen

shared glyph. You may have also seen these in certain programming oriented fonts which often use liatures for symbols like this. Using liatures, we can create a unique symbol for each of these different sequences of characters, all 60 that you can represent in the

second place. Making fonts that can take

second place. Making fonts that can take advantage of this is a bit of a pain.

The best app I found to work with these is glyphs. It's also what I use to make

is glyphs. It's also what I use to make the font currently animating on the screen. So, for example, let's take

screen. So, for example, let's take these digits 0ero through nine that I've defined. And let's just get rid of them.

defined. And let's just get rid of them.

Now, I'm going to reexport this font.

And back in Xcode, I'm going to rerun this example. We can now see the timer

this example. We can now see the timer plane with the regular digits. It's

still offset and clipped and the cropping no longer makes sense because we no longer have these fixed digit widths, but it's fine. Let's look at an example of adding a liature. I'm going

to create a new glyph and let's name it liature 12 cuz I want to see if we can get this to display when the digits 1 and two are displayed together. And now

we'll edit this. I'm going to add a new layer. It's going to be an eye color

layer. It's going to be an eye color layer. That's what iOS uses for emojis.

layer. That's what iOS uses for emojis.

And I'm going to drag in our cowboy hat emoji. Cool. Now we actually need to

emoji. Cool. Now we actually need to tell the font to use this liature if it sees the numbers one and two placed together. So I'm going to go up to font

together. So I'm going to go up to font info. I'm going to go to features and

info. I'm going to go to features and I'm going to add liatures. And I'm going to say sub as in substitute one two.

Those two characters in a row. And I'm

going to say replace it with liature 12.

Oh, I guess this only works if our font actually defines what one and two look like. So, okay, let's add those real

like. So, okay, let's add those real quick. And I think I'm just going to

quick. And I think I'm just going to make the digits one and two very, very crudely like this. Cool. Let's try

exporting the font again. So, let's

rerun Xcode. What we should see is our custom one and two. Just like that. But

when we actually get to 12, the font is going to recognize that we have a special character to show when one and two are placed together and show a different frame as a result. There we

go. So, that's a big insight right there. We can use liatures to assign

there. We can use liatures to assign frames to any of the 60 values that the seconds place can show. In fact, I think you could even do longer by creating liatures that include the minutes placed, a colon, and then the second's place. But point being, we can

place. But point being, we can definitely create more digits than just the 10 we'd be limited to by replacing the glyph 0 through 9. Now, there's an entirely different technique we can use for animations here that builds on top of these liatures. Let's go back to

glyphs. I'm going to delete my terrible,

glyphs. I'm going to delete my terrible, terrible glyphs, and let's generate empty glyphs for 0 through 9. Cool. So

right now our font's going to show an empty space for any of these characters 0 through9 space or colon completely empty for a timer. Now just like before I'm going to create a new glyph to use as a liature. And actually let's create two. I'm going to create an empty one

two. I'm going to create an empty one and a full one. And I'm going to open the full one. And I'm just going to put a box in here that fills up the entire area. And I'm going to mark it as fill

area. And I'm going to mark it as fill down here. Now if we go back here, we

down here. Now if we go back here, we have two characters, a completely empty box and a completely full box. I'm going

to go back to our liatures menu. And

this time I'm going to say a zero and zero together should display as this full box. and a zero and one together

full box. and a zero and one together should display as that empty box. And we

can just repeat this all the way down, alternating between full and empty boxes. And I'm going to keep that going

boxes. And I'm going to keep that going for all the options that the seconds place can display in this timer. Let's

go ahead and export. And let's run again. And now what we see is a box

again. And now what we see is a box blinking on and off. This makes sense, right? If I get rid of this custom font,

right? If I get rid of this custom font, again, this is just a timer. It's

counting 0 0 1 02. But with the custom font, our font sees 0 and it says, "Oh, I should display a full box." And then it sees 01 and it says I should display an empty box. And so what we get is this

one frame per second blinking animation.

And just like before, if we have a view that is blinking on and off, we can use it as a mask for some other content creating a frame blinking on and off.

And if we wanted two of these, all we have to do is have two different frames and then we need the timers to blink offset from each other. Both these

timers are counting up from a date we provide to them. Both of them are just using the current date, but we can change that. We can say the other timer

change that. We can say the other timer should start counting from 1 second ago.

And what you end up with then is two blinking boxes perfectly out of sync used as masks for two different images.

Now we have an animation that's running at one frame per second just like before. But instead of having to encode

before. But instead of having to encode all those different smiley images in a font like this, we have a single font that can be reused in a bunch of different contexts. These images can

different contexts. These images can even be dynamic. They can come from the internet, come from the user, and we can show them on and off like this. If we

want to show more frames, we would just use a different font. Instead of

blinking on for a second, then off for a second, we could have it blink on for a second, and then off for 3 seconds. That

gives you time to have four different frames all offset from each other. But

we're not done yet because this plus one is super interesting because we've been saying this entire time that timers can only run at one frame per second. And

that's true. Like they'll only ever update once per second. But we're in control of when that second starts. Look

here. I'm going to change this to a vertical stack so we can see both use it once. Let's get rid of the smiles too. I

once. Let's get rid of the smiles too. I

just want to look at the boxes. So we

have the same masks that we had earlier just blinking offset from each other.

But again, we can choose when their cycles start. We can choose the second

cycles start. We can choose the second one to be just a fraction of a second off from the first. That sounds

interesting because it gives us some control that's more granular than a second, but it doesn't immediately help us a ton. It doesn't matter if we start our animation with the top box or the bottom box. If we pick either of these

bottom box. If we pick either of these options, we still end up with an animation that's running at a frame per second. But what if instead of using

second. But what if instead of using either of these options, we use both? If

I take this second timer and I use it as a mask for the first timer, meaning we only see the final box when both of those timers are blinking on, this frame is visible for less than a second. In

fact, let's reduce the overlap even more so they're just blinking on together for a fraction of a second. That's a frame.

That's a frame using the public timer API. Let's use this to make a proper

API. Let's use this to make a proper animation. First of all, we have two

animation. First of all, we have two very similar timers here. I'm going to factor these out into a new view class.

We'll call it a blinking view. And let's

use a geometry reader to make all these sizes dynamic. So, first I'm going to

sizes dynamic. So, first I'm going to get the maximum size on either axis for this view. And we'll use that as the

this view. And we'll use that as the size for our font and our clipping and everything we have before. And then just like before, we're going to clip our container here so that all the other nonsense going on with the label isn't visible. Now, the only other thing we

visible. Now, the only other thing we need here is an ability to pass in an offset that determines when this blinking phase is going to start. I'm

going to call that blink offset, and we'll go ahead and take it in an initializer. We could just subtract this

initializer. We could just subtract this here, but that means that offset is going to be relative to whenever this view is created. But I don't want the time that the view is created to actually matter. I want this to be

actually matter. I want this to be offset from some shared date that all these views use. So, I'm just going to create a reference date up here. And now

we'll use that shared reference state minus whatever offset is passed to this particular view to determine when the blinking should start. And that means we can rewrite our whole view up here as a single blinking view plus another blinking view that is8 seconds offset

from it. So now we can write a single

from it. So now we can write a single blinking view like this. And if we want our faster blinking view, we just have to mask it with another one that's the same size but this time offset from the first. Now just like before, we have two

first. Now just like before, we have two blinking views. They're only overlapping

blinking views. They're only overlapping for a fifth of a second. We're going to have a bunch of these. So let's actually simplify even further. I'm going to rename this to a simple blinking view.

simple because this one blinks on for a full second. And then let's make a

full second. And then let's make a better blinking view that uses the same concept that we have up above. In fact,

I'm just going to steal it. And this

one's going to take in a blink offset just like the simple one, but it'll also take in a duration. And the

implementation is going to look basically like what we have up above already, where we have a simple blinking view offset to whatever was passed in.

And it's going to have a mask of a different blinking view where the two only overlap for however much duration we want. Right? So if we pass in a

we want. Right? So if we pass in a duration of 02 here, that means it's going to be offset by8 seconds, meaning they only overlap for 02. And now we can write this original one even more easily where we have a blinking view. We want

it to blink for 02 seconds and it should have a width and height of 100. And now

the rest of this will just figure it out. And if we want a slower blink, we

out. And if we want a slower blink, we just have to update our duration and we get it. This won't work for all cases.

get it. This won't work for all cases.

We wouldn't be able to pass in a duration of greater than two here. You

could actually have logic to support that if you want to though. Like you

could build any pattern out of these nested timers. But for now, this looks

nested timers. But for now, this looks good. Let's turn it into an actual

good. Let's turn it into an actual animation. So, just like before, we're

animation. So, just like before, we're going to create a Zstack of images. And

for each of these, we're going to create a single image, and we're going to mask that image to one of these blinking views. And up here, let's compute the

views. And up here, let's compute the duration of an individual frame. Let's

say we want this to run for 2 seconds.

So, I'm going to take two divided by frame count. And now, each view should

frame count. And now, each view should be visible for that duration. And its

frame should start at whatever index it is times that duration. Right? First

frame should start at zero. Next should

start at 0.25, so on. And if we rebuild this, that's awesome. We have an honest to god widget animation running here.

Just using timers. just using timers that can only update once per second.

With the right configuration, we can get an animation running at a decent frame rate. In fact, let's see how fast we can

rate. In fact, let's see how fast we can go. If we say we want the animation to

go. If we say we want the animation to take 1 second instead of two, we have a slight issue here, which is that we play our eight frames quickly and then they disappear for a second, which makes sense. Remember, we're controlling the

sense. Remember, we're controlling the animation here by taking the overlap of two timers where the timer blinks on for a second and off for a second. It has a total period of 2 seconds. By taking the overlap, we can get it to flash on for a fraction of those two seconds. But if we

play our entire animation in 1 second, then there's nothing to show for the remainder of that 2C period. There is an easy way for us to fix this, which is to just create a duplicate set of frames for that remaining second, where frames 0 and 8 look the same, 1 and 9 look the

same, even if they're two separate images under the hood. And now we can see the animation's running pretty well.

Can we get even faster? Let's try a quarter of a second. Quarter of a second divided by 8 means we're running at about 30 frames per second. You can go pretty fast with this. You can actually go faster than the clock style animations, which are capped to 20 FPS.

So, this is awesome. I don't know if you can tell over the video, though. There's

a little bit of glitchy behavior where there's some frames where it looks like maybe nothing's visible at all. And the

reason for that is that as close as we're getting with these timers, they're not perfect. We're trying to use a timer

not perfect. We're trying to use a timer to hide a frame right as the next one appears. But if we hide a little too

appears. But if we hide a little too early or we show a little too late, that means there's a gap where nothing's visible at all. This is about as good as we can do for images with transparent backgrounds. But for other cases, we can

backgrounds. But for other cases, we can actually do a little bit better. Let's

actually go back to our simplified version running at a slower frame rate.

You can even see the glitchiness a little bit here if you're watching at 60 fps. So here's what we can do. Instead

fps. So here's what we can do. Instead

of trying to hide the last frame at the exact same point when we show the next frame, let's actually continue to leave the previous frame visible. Let me give an example that. So instead of passing in a duration here, let's actually switch back to using our simple blinking

view where each frame is going to be visible for an entire second. Let's take

a look at how that looks first. Okay, so

we can see what we meant with transparent backgrounds here. Using the

strategy, obviously you can continue to see the frames behind whatever the latest frame is, but we can fix that.

I'm going to go to the XC assets directory where all these emojis are stored, and I'm just going to recursively add a white background to all of them. There we can see it took effect and Xcode already. And let's work with those going forward. Cool. So now

we can see our images stacking on top of each other just like before, but because of the opaque background, we can't see whatever frame was prior. We do still have a problem though, which is that whatever frame comes last, in this case, the melting face, ends up hanging around for a full second because there's

nothing on top of it to cover it up. And

we can't solve that by adding more frames because whatever the last one is will then end up being the thing that sticks around for too long. But here's

what we can do. I'm actually going to split this into two different stacks.

One that shows the first half of the images and one that shows the second half of the images. and we'll wrap all that in a stack on top of each other. So

if we run this again, we should see the exact same thing as before. The smiles

mostly working and then the melting face hanging around at the end. But now that we have two different stacks, we can actually say mask the entire second stack with another blinking view. And

I'm going to have this one offset by a full second. And think about what this

full second. And think about what this means. If we add this modifier to a

means. If we add this modifier to a view, that means it's going to be visible from t= 1 to t equals 2 and then it's going to go away. And with that, everything runs perfectly. The first

four frames appear over the first second. The next four frames are stacked

second. The next four frames are stacked on top in the following second. And then

when it's time to loop, those last four frames disappear. We can see the bottom

frames disappear. We can see the bottom stack again where frames again start to get stacked on top of each other. And

this way if any of those timing operations is a little off, a frame disappears too early or appears too late, it doesn't matter. The frame

before will still be visible. And we can see this again if we crank up the frame rate. Let's actually explicitly say that

rate. Let's actually explicitly say that we want 30 frames per second. And that

means we're going to need 60 frames because we always need at least 2 seconds worth of frames. And if we try running this, that is beautiful. I hope

it's coming through over the video. This

is running at a proper 30 frames per second. No visual glitches anymore. And

second. No visual glitches anymore. And

again, just using timers, which run at one frame per second, no clocks, no private APIs. Now, if you're wondering,

private APIs. Now, if you're wondering, can we do even better? Yes, we're not done yet because of this. This animation

I showed at the very start of the video.

Something's weird about it, and it's getting even weirder the longer it plays. See, there's obviously still some

plays. See, there's obviously still some limitations here. The big one is that

limitations here. The big one is that much like there was a maximum number of clocks you could have on screen before the phone started to get really upset with you, there's also a limit to the number of timers you can have. Now, the

timers actually behave a lot better.

Your phone will start to get visibly upset the more clocks you have. But

timers actually work pretty well up until 150 to 200, at which point they just entirely stop working. But that

means there's a similar limitation here in the maximum number of frames you can have before an animation starts to break. At 30 frames per second, that's

break. At 30 frames per second, that's maybe 5 seconds of animation. Now, this

animation's slower. It's only running at 8 frames per second. But beans the cat has been running around doing her little tasks for a long time now, and she's somehow still going. So, what gives?

Well, this example is about as good as you can get while allowing fully dynamic images to be displayed. Like if you wanted to convert userp provided video to an animation for the lock screen, this is how to do it. But if you know in advance what frames you want to show, we

can actually do significantly better here. We're currently using our blinking

here. We're currently using our blinking view to show an image for a fraction of a second. But there's nothing that says

a second. But there's nothing that says that has to be an image. We could put a shape here or a button or a label. This

is actually a good way to figure out that our animation's been running backwards this whole time. Let's fix

that. Cool. So, if we could put a label here, that means we could also put another timer. And let's give each timer

another timer. And let's give each timer a random color so we can tell them apart. Now, when we run this, we can see

apart. Now, when we run this, we can see the colors flashing, meaning we're switching between all these different timer instances. But we can also see

timer instances. But we can also see that that collective group of timers is counting up like we'd expect. We have

eight timers that each show for a quarter of a second. That means when we show the very first timer, it has 0 0 in the second place. And then we cycle through all the remaining timers. We go

back to showing the first timer and now it's been 2 seconds. So that timer shows 02. And because we know how to use

02. And because we know how to use custom fonts to control how 0 0 is displayed versus 02, we can actually use that timer to show one frame the first time it appears and a different frame the next time it appears. And we can do

that for all the timers. So I've created 16 different fonts that we are going to plug into these timers, each of which contains every 16th frame of this animation. Does that make sense? Like

animation. Does that make sense? Like

here, here's font number one. It

contains every 16th frame. If I rerun this using font number two, you're going to see all the frames that were directly after the frames from the last font.

There are 16 fonts and they each contain every 16th frame all offset from one another. And that means if we go back to

another. And that means if we go back to our fast emoji animating code. So this

time instead of 60 views, we're going to have 16, one to hold each of our different fonts. And we're going to

different fonts. And we're going to explicitly set the frame rate to an eighth of a second. And now again, instead of each of these images, we're now going to show a timer. And each of these labels is going to get its own custom font based on its index. And then

we're going to use the same trick we used before to make sure we're reserving enough space for all the timers digits and that the second's position will be in a predictable spot. and then shift everything over so that the second's position is in the center of the frame.

Now, right now, all these individual timers are starting at the exact same time. But really, we want each of them

time. But really, we want each of them to start as far away from a frame change as possible, right? Like if the first timer becomes visible at t= 0, t= 2, t= 4. We don't want its content changing at

4. We don't want its content changing at that time. We'd rather it switch its own

that time. We'd rather it switch its own frame at t equals 1 when it's not even visible. So, to account for that, we're

visible. So, to account for that, we're going to offset each of these frames switching by 1 second plus the amount of time it takes for that timer to actually come on screen. Like the second timer's transition should happen a quarter of a second after the first. This is looking

pretty good. I'm going to copy this down

pretty good. I'm going to copy this down here again and let's try running it. All

right, build succeeded. Fingers crossed.

Okay, we're almost there. We forgot we needed to reverse the order of these frames again. And now we've made a very

frames again. And now we've made a very complicated setup, but it totally works using only 17 different timers. We have

a single animation running for 30 seconds perfectly in a loop in what I think is the most advanced animation to ever appear in an iOS widget. This time

with zero private APIs and zero clocks.

This was a long journey, but I sincerely hope at least somebody watching this is going to make the world's coolest widget as a result. And even though we found a way to do this just using timers, I have just brought a lot of attention to a private API that at least a few apps are

using. And one outcome here is that

using. And one outcome here is that Apple starts shutting those down. They

might even shut this workound down, like make it impossible to use a timer for a mask, but I really hope they don't. I

think there are actually a lot of apps that would see benefit from animations on their widgets. There are definitely apps where I would love to see them. And

widgets are opt-in, so you could always not use a widget if the animations were implemented in an annoying way. So, I

really hope Apple keeps these APIs open and maybe even expands widgets animation capabilities in the future, so we can have all these cool apps and more instead of just this one. Thanks for

watching. I'll see you next time.

Loading...

Loading video analysis...