Menu
O mnie Kontakt

Bryce Bostwick w swoim najnowszym filmie eksploruje wtyczki iOS oraz ich nieodkryte możliwości, które dotyczą animacji nektórych widgetów. Już na początku stwierdza, że niektóre zegary widgetowe powinny być niemożliwe do zrealizowania z punktu widzenia dewelopera, a mimo to Apple stworzyło zaawansowane animacje dla swoich aplikacji. Zaczyna od omówienia udostępnionej przez Apple prywatnej API, która umożliwia tworzenie dobrze animowanych widgetów, mimo że firma postanowiła nie zezwalać na to innym deweloperom. Takie działanie to typowy przykład na to, jak Apple, mimo regulacji, wprowadza wyjątki dając sobie przewagę konkurencyjną. W filmie mówi o sposobach, w jakie deweloperzy mogą obejść ograniczenia, wykorzystując różne techniki animacji, które opierają się na użyciu etykiet z timerem.

Bostwick przedstawia również, jak można animować widgety na podstawie timerów, które co sekundę aktualizują swoje layouty. Tu zyskuje ciekawy pomysł na wykorzystanie znaków typograficznych do tworzenia animacji, które bazują na ligaturach fontów. Odwołuje się do wykorzystania timerów, które update'ują co sekundę, ale można ich używać w bardziej kreatywny sposób. Celem jest stworzenie animacji, które są na tyle płynne i czyste, aby zachwycały użytkowników. W swoim filmie porusza również wyzwania związane z ograniczeniami technologicznymi, które są obecne przy budowie zaawansowanych widgetów.

Twórca podaje kilka skrótów i trików jakie pozostali deweloperzy zaczęli ujawniać w kontekście animacji widgetów. Używa Xcode oraz innych narzędzi programistycznych do zakupu firmware, aby zagłębić się w jego strukturę i zrozumieć, jak dokładnie zbudowane są widgetowe zegary. Mały wgląd w strukturę systemu operacyjnego i możliwości, które deweloperzy mogą znaleźć dla siebie to ostateczny cel tego materiału. Co więcej, porady te są konkretne i można je łatwo wykorzystać w praktyce, zwłaszcza dla osób, które tworzą aplikacje mobilne.

Na końcu Bryce przedstawia parę problemów, które mogą wyniknąć z intensywnego używania nieoficjalnych API. Ostrzega przed późniejszymi zmianami ze strony Apple, które mogą zablokować pewne techniki i rozwiązać okazje, które zyskały popularność po jego filmie. Niezależnie od tego, czy jego podejście zostanie w przyszłości przyjęte przez Apple, wygląda na to, że w ciągu tych kilkunastu minut pokazał na co stać widgety iOS, przynajmniej za pomocą programowania SwiftUI. Z radością można zauważyć, że jego film ma już ponad milion wyświetleń, co świadczy o jego popularności oraz zainteresowaniu tematem animacji widgetów na urządzeniach mobilnych.

Podczas pisania tego artykułu film Bryce'a ma na koncie 1028009 wyświetleń oraz 42165 polubień. Tego rodzaju zaawansowane animacje i odkrycia przyciągają wielu pasjonatów i programistów, którzy chętnie dzielą się swoimi pomysłami. Prace takie mogą być inspiracją dla innych, by wykorzystać możliwości animacji do wzbogacenia interfejsów użytkownika w aplikacjach mobilnych. Z perspektywy technicznej, analizy i eksperymenty Bostwicka stają się widoczne jako wartościowe informacje, które przyciągają uwagę między innymi klientów Apple.

Toggle timeline summary

  • 00:00 Wprowadzenie do zaawansowanych widgetów zegara na iPhonie.
  • 00:06 Dyskusja na temat tylnej furtki Apple dla animowanych widgetów.
  • 00:10 Wyjątek poczyniony dla Apple, aby mogło używać animowanych widgetów.
  • 00:31 Wyjaśnienie zwyczajowych praktyk Apple dotyczących ograniczeń dla programistów.
  • 00:39 Przykłady skomplikowanych rozwiązań dla animowanych ikon aplikacji.
  • 00:51 Dezkompilacja wewnętrznych metod Apple dotyczących zachowania widgetów.
  • 01:11 Obserwacje na temat tego, jak inne aplikacje wykorzystują tylne furtki Apple.
  • 01:30 Krótki przegląd ograniczonych możliwości animacyjnych iOS 17.
  • 01:48 Wyjaśnienie procesu wysyłania układów do widgetów.
  • 02:10 Wyzwanie związane z efektywną animacją widgetów.
  • 02:55 Tworzenie timera w celu ułatwienia animacji.
  • 03:24 Wprowadzenie etykiety działającej jako timer dla widgetów.
  • 03:58 Demonstracja sposobów dostosowywania etykiet timerów.
  • 04:40 Pokazywanie, jak zarządzać pozycją cyfr timera.
  • 05:15 Porównanie kontroli widgetów z standardowymi procesami aplikacji.
  • 06:33 Badanie ograniczeń wydajności widgetów w zakresie animacji.
  • 08:02 Odkrywanie złożoności związanych z trikami animacyjnymi.
  • 09:21 Ujawnianie informacji o głębokości animacji na podstawie zegara.
  • 11:20 Rozpoczęcie prac nad ramami do rotacji wskazówki zegara.
  • 13:20 Wyjaśnianie pojęć zagnieżdżonej rotacji dla efektów animacyjnych.
  • 15:50 Podsumowanie na temat unikalnych metod wykorzystywanych.
  • 36:20 Zachęta dla widzów do eksploracji zaawansowanych projektów widgetów.
  • 36:56 Zamknięcie i zaproszenie dla widzów do powrotu.

Transcription

This clock should not be possible to 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 gonna believe this. That is a clock. Apple left a backdoor in their widget 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 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 backdoor. A private API that can be used to make this clock, or this clock, or this 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 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 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 backdoors 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 apps. Now we're gonna 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 bare-bones API for doing some kinds of animations, but that's not what I'm 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 two 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 indeterminate time asks your app, hey, what do you want to show in your widget? And your app responds with whatever layout you want to put on 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 SwiftUI 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 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 five 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 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. IE and 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 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 instead of the numbers zero through nine 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 and 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 little bit. I'm gonna take all this and I'm gonna 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 gonna 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 have enough room to show nine digits. Now we wanna make sure the ones digit is in a consistent spot because right now, once the timer hits 10 minutes, everything's gonna 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 ones place is always gonna be at the very end. And now I wanna shift the entire label so that that ones place is actually in the middle. In this case, that means I'm gonna 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 gonna 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 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 wanna 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. 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 custom fonts. There's a pretty big difference between these two clocks though. This one runs at one frame per second, but this one runs at a buttery smooth 20 FPS. 20 FPS. Look, 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 implementation. We can 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 Blacktop's IPSW tool. It's just one of the most useful iOS tools out there. It's great. So let's 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 little bit. Now that that's done, I'm gonna 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 SSH into a jailbroken 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 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 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 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 just gonna take the subtitle here, the one that says display the current time. And I'm gonna go ahead and grep for that within this file system. And I'm using the binary flag to indicate we also wanna search in binary files. And here we go, we got two results. They both live in an app called MobileTimer, 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 like where the widget actually lives. Now we wanna poke around at this widget in a disassembler. So I'm gonna go ahead and open this directory in 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 into our disassembler. And all the default options here should be okay for our case. Now widgets are implemented in SwiftUI, and SwiftUI is awful to 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 of 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 WidgetKit framework, we see a very interesting one, underscore clock hand rotation effect. This sounds like it's probably the thing making clock hands 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 find this symbol organically if you were trying to reverse engineer this for 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, macOS will stop me, saying that it is simply too old. You expect me to open software from three 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 new project, Xcode crashes. So we're going to have to open it in a virtual machine running an old copy of macOS 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 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 boilerplate for 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. 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 five seconds. We want it in the current time zone. And this, 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 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 was that this was only public for a year 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. 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 ClockHandRotationEffect. 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 this view modifier, which determines what this modifier does, I'm going to use it to add the private ClockHandRotationEffect. And for now, let's just hard code some values here. So what does 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 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 ClockHandRotationEffect function that takes in that time interval, where this extension just automatically adds that modifier to a given view. Now one last thing over an old macOS 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 terminal. The next five seconds aren't super interesting, just including for 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, because now we can take that framework and drag it out of our old copy of macOS and into our modern day 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 screaming at their computers that that was a bad idea. Now, if we add that framework to our project and we go ahead and import it at the top of this file, Xcode still won't be able to see this Apple provided underscore clock hand 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 gonna switch back to a regular font and then let's go ahead and build and 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 re-exposes that private API. Now, if you're wondering, okay, we have a single API that creates a steady 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 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 little bit. We're gonna have a single rectangle. We'll give it a background color of red and a fixed width or 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 original API, but they didn't appear 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. 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 different point than just the view center. And a question you may have been wondering already, you can indeed nest these, meaning we can have this inner object spin while also rotating on a 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 Fourier 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 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. Let's stack our inner circle on top 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, 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 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 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 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. We have an inner circle of half its 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 a clock hand rotation effect to that 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 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 circles, but it's rotating at a constant rate. So we can actually counteract that using one more clock hand rotation effect. And now you have a perfect smile moving across the screen on a fixed 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 the 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 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 normal code, super hard to create using 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 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 some other options. This is probably a good time to talk about Top Widgets. This is the app we looked at a couple of videos ago because it included a surprising number of anti-debugging techniques, including some outright 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 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 gonna take a look at what these apps are currently doing, but don't worry. After that, we're gonna extend it further with a better system that I don't think anyone's actually figured 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 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 gonna define how many slices we want. Let's just say eight. If we have eight slices, that means that each one is gonna take up an eighth of 360 degrees. And then for each of those slices, let's go ahead and draw a shape. And then I'm gonna give each one a random color as well. Cool, 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 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 whole construct down a little bit? Let's say 1,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 whole view is moving a good amount faster. What if we go even further? 10,000 pixels shifted instead of just 1,000. 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's invisible. Mostly, it's just a quick switch between 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 way up these slices. At this point, there's zero visible movement. There's no longer any context of rotation. 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 gonna switch to this showing a single slice. Cool, there's our one slice moving around. SwiftUI 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 gonna move 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 little bit. Instead of one rotating collection full of slices, let's actually give each slice its own individual rotation. So I'm gonna 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 wanna do is add a mask to each one where each of those masks is a 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 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 clock 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. That's what I'm saying! So that's how this style of animation works. We are blatantly abusing a backdoor left-in by Apple in one of the coolest ways I've 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 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 the future, but it's also just a huge, huge help to me and the channel, so 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 timer-based 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 we're limited to just 10 unique frames here, because this is a font where we've replaced the digits zero through nine. But we actually have a lot more options here, thanks to font ligatures. 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 these in certain programming-oriented fonts, which often use ligatures for symbols like this. Using ligatures, we can create a unique symbol for each of these different sequences of characters, all 60 that you can represent in the seconds place. Making fonts that can take advantage of this is a bit of a pain. The best app I've found to work with these is Glyphs. It's also what I use to make the font currently animated on the screen. So for example, let's take these digits zero through nine that I have to find, and let's just get rid of them. Now I'm gonna re-export this font. And back in Xcode, I'm gonna rerun this example. We can now see the timer playing 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 ligature. I'm gonna create a new glyph, and let's name it ligature 12, because I wanna see if we can get this to display when the digits one and two are displayed together. And now we'll edit this. I'm gonna add a new layer. It's gonna be an eye color layer. That's what iOS uses for emojis. And I'm gonna drag in our cowboy hat emoji. Cool. Now we actually need to tell the font to use this ligature if it sees the numbers one and two placed together. So I'm gonna go up to font info. I'm gonna go to features, and I'm going to add ligatures. And I'm gonna say sub as in substitute, those two characters in a row. And I'm gonna say replace it with ligature 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 quick. And I think I'm just gonna make the digits one and two very, very crudely like this. Cool. Let's try exporting the fonts again. So I'll rerun an Xcode. What we should see is our custom one and two, just like that. But when we actually get to 12, the font is gonna 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 ligatures 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 ligatures that include the minutes place, a colon, and then the seconds place. But point being, we can definitely create more digits than just the 10 we'd be limited to by replacing the glyphs zero through nine. Now there's an entirely different technique we can use for animations here that builds on top of these ligatures. Let's go back to glyphs. I'm gonna delete my terrible, terrible glyphs. And let's generate empty glyphs for zero through nine. Cool. So right now our font's gonna show an empty space for any of these characters, zero through nine space or colon. Completely empty for a timer. Now, just like before, I'm gonna create a new glyph to use as a ligature. And actually let's create two. I'm gonna create an empty one and a full one. And I'm gonna open the full one and I'm just gonna put a box in here that fills up the entire area. And I'm gonna mark it as fill down here. Now if we go back here, we have two characters, a completely empty box and a completely full box. I'm gonna go back to our ligatures menu. And this time I'm gonna say a zero and zero together should display as this 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 gonna 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 blinking on and off. This makes sense, right? If I get rid of this custom font, again, this is just a timer. It's counting zero, zero, zero, one, zero, two. But with the custom font, our font sees zero, zero and it says, oh, I should display a full box. And then it sees zero, one 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 of 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 should start counting from one 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 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 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 three 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. Like here, I'm going to change this to a vertical stack so we can see both views at 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 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 options, we still end up with an animation that's running at a frame per 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 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 get the maximum size on either axis for this view. And we'll use that as the size for our fonts and our clipping and everything we had 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 is invisible. 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 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 offset from some shared date that all these views use. So I'm just going to create a reference state 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 is 0.8 seconds offset 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 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 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 duration of 0.2 here, that means it's going to be offset by 0.8 seconds, meaning they only overlap for 0.2. And now we can write this original one even more easily where we have a blinking view. We want it to blink for 0.2 seconds and it should have a width and height of 100. And now the rest of this, we'll just figure it 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. 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 good. Let's turn it into an actual animation. So just like before, we're going to create a Z stack 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 duration of an individual frame. Let's say we want this run for two seconds. So I'm going to take two divided by 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 go. If we say that we want the animation to take one 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 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 two 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 one second, then there's nothing to show for the remainder of that two second 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 frame zero and eight look the same, one and nine 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 eight 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 to hide a frame right as the next one 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 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 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 of 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 this 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 in 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 phase, 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 smile's mostly working and then the melting phase 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 means. If we add this modifier to a view, that means it's going to be visible from T equals one to T equals two, 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 on top in the following second. And then when it's time to loop, those last four 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 it will still be visible. And we can see this again, if we crank up the frame 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 two 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 again, just using timers, which run at one frame per second, no clocks, no 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 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 maybe 5 seconds of animation. Now this animation is slower. It's only running at 8 frames per second, but it means 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 user 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 view to show an image for a fraction of 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 has 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 a random color so we can tell them 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 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 zero zero in the seconds place. And then we cycle through all the remaining timers, we go back to showing the first timer, and now it's been two seconds, so that timer shows zero two. And because we know how to use custom fonts to control how zero zero is displayed versus zero two, 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 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 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 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 position will be in a predictable spot and then shift everything over so that the second 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 to start as far away from a frame change as possible, right? Like if the first timer becomes visible at t equals zero, t equals two, t equals four, we don't want its content changing at that time. We'd rather it switch its own frame at t equals one when it's not even visible. So to account for that, we're going to offset each of these frames switching by one 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 here again and let's try running it. All right, build succeeded. Fingers crossed. OK, we're almost there. We forgot we need to reverse the order of these 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 Apple starts shutting those down. They might even shut this workaround 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.