Aug 29 2020 10:00 AM
One of my favorite new things announced during this year’s WWDC was App Clips. They allow developers to offer a small experience from their app to users, without the need to install the entire app from the App Store.
App Clips can be activated through a variety of methods: NFC tags, QR codes, special App Clip codes from Apple, or through a simple link somewhere, like your app’s website.
When I saw the announcement, I immediately had the idea to create an App Clip for my app, ChibiStudio. It’s a great way to show users what the app has to offer before they actually commit to downloading the full app.
ChibiStudio is an app that allows users to create their own chibis by selecting from a variety of items, similar to other avatar creation apps or even Memoji. I knew that would be the experience we’d like to turn into an App Clip.
But putting the creation experience from ChibiStudio into an App Clip proved to be a significant challenge. One of our goals with ChibiStudio has always been to make as much of the app usable offline as possible, which means that we ship all of the assets required to enable chibi creation within the app bundle itself, we don’t download that after the fact.
App Clips are limited to 10MB in size after going through app thinning though, so the main challenge with creating the chibi editing experience for the App Clip was finding ways to reduce the size of the app as much as possible.
Check out the result below and read on for the technical details:
Making the inventory fit
The first thing we had to do was to reduce the size of the item inventory itself. The full app has thousands of items split into a little over 20 item packs, some of which are premium, which means they’re unlocked by in-app purchases.
Since App Clips can’t have in-app purchases, all of the premium packs were removed. But even with all of them gone, the free packs alone were about 50MB in size. I asked my artist to pick just two packs and remove a bunch of items from them, anything that wouldn’t hurt the experience too much.
The tricky part was that he wouldn’t be able to tell exactly how much smaller the packs were getting, since they’re compiled before being added to the app, and that compilation step is only done by me. So after receiving the two reduced packs from him, I compiled them, but they alone were over 12MB in size. Ouch!
Here I must explain a little bit about how the packs are compiled. A sqlite database is exported which contains metadata about all packs and items, but that’s very small (less than 100KB). The actual assets are compiled into asset catalogs and contained in bundles (a bundle for each pack). Each item in a pack is comprised of two assets in the catalog: a data asset containing the compressed vector data (Core Animation archive) for the item — used in the canvas — and an image asset containing a preview image for that item — which is used in the item drawer.
Reducing vector data size with LZMA compression
The first thing I did was to change the compression used for the vector data of the items. I was using zlib (gzip) since day one of the app, but I had a look at Apple’s Compression framework and noticed that the LZMA algorithm promised a high-compression ratio, so I decided to try that out.
When compiling all of the packs for the main app, using the LZMA algorithm reduced the full size of the inventory from 127.6MB to 78.5MB, without impacting runtime performance in any noticeable way. That’s great, since it reduces the size of the main app by a lot, but even then, the size of the reduced packs for the App Clip was still a little over 8MB by itself, and that’s not counting all of the code and assets that would have to go into it 😬.
Rendering item previews on the fly
After looking at the compiled asset catalogs, I noticed that most of their file size was due to the preview images, so I decided to see what would happen if I just removed them. Initially, the item drawer was just showing a bunch of blank squircles, which makes sense since there were no preview thumbnails to be used, but my idea was to instead generate the thumbnails at runtime.
I had tried that out before, but it was way back in the iOS 10 days when devices were much slower. It turns out that it’s perfectly viable to render the previews for items on the fly. I’m still not sure if I’m going to stop including the pre-baked previews in the main app, but at least for the App Clip, I’m rendering those at runtime.
For those who are curious about how it’s done in practice, there’s nothing special about it. For every item that needs to be rendered, the cell will request the image from the inventory, which owns a custom serial dispatch queue where work items are executed to perform the rendering, which is done with
UIGraphicsImageRenderer. From the point of view of the cell, it looks very similar to downloading an image from the internet, so the same caveats about doing asynchronous work related to recyclable cells apply.
The resulting image for each item is cached in memory using
NSCache, but I could probably also cache it using file storage so that it would persist between different runs of the app.
Checking the thinned App Clip size locally
As much as I would like to tell you that all of the work described above was enough to make the whole chibi canvas experience fit in under 10MB, unfortunately I can’t, because that was only half the battle.
After doing all of that work and actually creating the App Clip experience, which was quite easy because I was mainly leveraging existing work from shared frameworks within the app, I decided to check how I was doing in terms of App Clip size.
The 10MB limit applies after app thinning, so to test that out locally we need to archive and export the app for Ad Hoc distribution. When picking that option, Xcode will actually offer to export the app or the App Clip, so I chose that option and selected “All compatible device variants” in the “App Thinning” dropdown.
Looking at the resulting IPA file — the one with no UUID next to it, which is universal — it was 9.5MB, just under the 10MB limit. So I thought I was ready to upload it to TestFlight (narrator: “he was wrong”).
After uploading to TestFlight and waiting a while for it to process, I got this lovely e-mail from App Store Connect:
Turns out, when talking about the 10MB limit, they’re not referring to the size of the compressed IPA file, but the size of the actual
.app package inside of it. So in order to verify that locally, we need to unzip the IPA file. After doing that for the build, this is what I saw:
So even though my IPA package was under 10MB, the App Clip itself after being extracted was over 13MB, I still had some work to do.
Making the code fit
We tend to underestimate how large code itself can get, especially when working with Swift. ChibiStudio looks simple on the surface, but it’s actually quite complex with lots of features, many of which are not related to the experience we wanted to provide in the App Clip.
The app is already broken down into multiple frameworks, which is how I like to work since it makes it easier to deal with app extensions and make sure things are organized properly, avoiding coupling where it’s not appropriate.
Unfortunately, over time, most of the app ended up being implemented inside the
CutenessUI framework, which is the framework that implements most of the user interface of the app, including basic components, most of the theming, and the chibi canvas itself, which is what we want in the App Clip.
Another side-effect of implementing a large portion of the app in that framework was that it also depends on other frameworks, and at least one of them will not be needed in the App Clip.
Below you can see a diagram of the app’s components, before the changes I’ve done to make the App Clip smaller:
As you can see,
CutenessUI had lots of things below it, and it was itself quite large. In order to reduce its scope for the App Clip, I decided to move the canvas to a separate framework, which I called
CutenessCanvas. This new framework couldn’t depend on
CutenessUI, but it needed some base UI code from it, so I moved that base UI code to yet another framework, which I called
CutenessUIFoundation. Doing that also removed the need to include the
Bengoshi framework (analytics and in-app purchasing code) from the App Clip, which doesn’t need that functionality.
This is what the modules look like, now with the App Clip and the UI frameworks split into multiple modules:
When comparing the previous build with the
CutenessUI framework, which was 3.8MB in size, the
CutenessCanvas frameworks in the new build add up to just above 2MB in size.
I still had to do some more work to reduce the size even more, which I accomplished by removing even more items from the reduced packs, and also finding some old unused code which was still being compiled into the core and UI frameworks.
After all of that work, I finally had a version of the App Clip which was just under 10MB — it ended up at 9.5MB. I’m sure I can still find some unused code in there somewhere and come up with other clever ways to reduce its size even more, but I’m happy with how it’s working so far.
As much as the work described in this post might sound like a chore, I actually had a lot of fun during this entire process. Making great things with constrained resources can be a fun challenge, so don’t give up if your initial attempts at making an App Clip prove difficult due to the 10MB limitation.
This post focused on making my particular app experience fit in an App Clip, but there’s a lot more to App Clips than just making them be under 10MB. One of them is testing, and for that I highly recommend this post from Kushagra.
And finally, if you’d like to try out the App Clip that I made for ChibiStudio, you can find it on TestFlight.