App Thinning in iOS 9 & NSBundleResourceRequest
By: Sam Solomon
App Thinning–a series of processes, tools, and techniques released with iOS9–seems quite self-explanatory, however, there’s several things going on behind the scenes. You, Xcode, and Apple’s App Distribution mechanism contribute massively to thin the app, which is important, because Apple’s flagship phones are still conforming to 2008’s storage standards by running 16GB base capacities.
Apps are usually bulky, containing resources, assets, videos, images, and other data that are bundled together and uploaded to the store. Among all of those resources, an iPhone 5S would never use 3x image sizes, and an iPhone 6 would never need to use 1x images. What falls between the cracks are images and resources that will never be used.
Creating a single archived bundle and exporting that to the store is now passé; referencing the App Distribution Guide, the App Store will now create individual app bundles based on the developer’s specifications by slicing assets and resources appropropriate to the device requesting the app.
Bitcode is an optional way of uploading your app to the store. If your app is uploaded to the store in bitcode, future updates and optimizations can be made automatically by the App Store itself, adding to any potential improvements that developers make to their apps.
The last of the ways you can continue to further thin your app is by using On-Demand Resources;
On-Demand Resources are similar to level packs, which are downloaded only when needed. This has come about after the release of tvOS, which mandated that the install size of any app cannot exceed 200MB. There are, however, much more relaxed restrictions on the app-size after install. This might seem over-the-top simply to get around a pre-existing issue, but the implications of having On Demand Resources are greater than it seems on the surface. These extended resources are hosted on the App Store, and are downloaded to the device’s local storage using an NSBundleResourceRequest. Let’s delve into Apple’s documentation, which for once, is actually readable (yeah, I know), and I’ll attempt to provide clarity wherever possible.
Setting up On-Demand Resources
Start by setting Enable On Demand Resources to YES in your target’s Build Settings, under Assets.
If you’ve used *.plist files with Xcode before, you’re likely to find the interface for creating asset packs and tagged assets very familiar and reflexive. If you haven’t, don’t worry about it.
Here’s a quick guide on adding and handling resources using the On Demand Resources model;
Go to the Resource Tags tab in your target.
At first, there’s nothing to be seen, not even a peep of a tag or a header. So go ahead and add a tag using the + icon.
Now, you have a placeholder New tag, with a bunch of categories to sort it under.
The categories are self-explanatory, but here’s a quick look anyway;
- Initial Install Tags The resources attached to this tag are bundled with the app and will be included in the app bundle during installation.
- Prefetched Tag Order Sometimes, you may want to have resources installed immediately after the app is launched for the first time. You create a set of tags that are fetched in the specified order at the first launch. An example would be a set of tutorial videos that help onboard the user to your application.
- Download Only On Demand Download Only On Demand is the core aspect of On-Demand Resources, where the tags you add can be fetched with an NSBundleResourceRequest.
Lets begin by setting up some tags that can be downloaded only on demand. With a tag ready, set up the resources for this tag using the + button.
Add the resources and assets to the tag using the provided drop-down menu. I highly recommend using Image Assets wherever possible, since the convenience this affords is something you should definitely take advantage of.
Sort your assets logically; For example, the Library-app has several readable books, but the covers are pretty large and have a high resolution. When the app is installed and launched, the book covers are not needed immediately. Instead, I’m using a placeholder, and loading the book covers right before the user peeks into the library. Sorting this logically would mean having an .xcasset for the book covers, like so:
Since the tag is filed under “Download Only On Demand”, when a user downloads the Library.app from the App Store, these assets are sliced from the initial install and fetched only when the app uses an NSBundleResourceRequest to fetch them. This table shows size limits for resources, tags and bundles:
The gist of the size limits is this;
- An iOS App Bundle cannot be larger than 2GB.
- A tvOS App Bundle cannot be larger than 200MB.
- A tag cannot contain resources larger than 512MB, while the total size of Initial Install Tags cannot exceed 2GB.
- Similarly, Initial Installed Tags and Prefetched Tags cannot exceed a total size of 2GB.
- When using a NSBundleResourceRequest to fetch/use tags, the total size of the tags currently in use cannot be larger than 2GB.
- Lastly, the total size of On-Demand Resources hosted on the App Store cannot be larger than 20GB.
Using NSBundleResourceRequest
So far, we’ve created Resource Tags and sorted them.
Referencing the Library.app and how we fetch our Book Covers when a book in the libraryis viewed, we can see how this request is used. A resource request is performed by specifying tags, optionally with a bundle. An NSBundle is something even novice iOS developers are familiar with, since it specifies a logical space where all your resources exist. You’re usually specifying the main bundle.
- (instancetype) initWithTags:(NSSet *) tags
— (instancetype) initWithTags:(NSSet *) tags
bundle:(NSBundle *) bundle
When the user taps on a book and is taken to its detail view, the image is loaded with a book cover that is a resource of the covers tag. To begin fetching the resource, create an NSSet containing the covers tag.
NSSet *tagSet = [NSSet setWithArray:@[@”covers”]];
Then initialize the request with the tags;
self.request = [[NSBundleResourceRequest alloc] initWithTags:tagSet bundle:[NSBundle mainBundle]];
Using conditionallyBeginAccessingResourcesWithCompletionHandler:^(BOOL resourcesAvailable) on an NSBundleRequestResource object allows you to check if the resources are locally available, indicated by a resourcesAvailable flag. If they are available, begin using them as if they were already available.
- (void) conditionallyBeginAccessingResourcesWithCompletionHandler:(void (^)(BOOL resourcesAvailable)) completionHandler
If not, then use beginAccessingResourcesWithCompletionHandler:^(NSError * _Nullable error) to fetch the resources from the App Store. With this network call, you get a nifty completion block to handle all your incoming assets/data, or an error when things get FUBAR real quick. Note that if resources are partially available, the request is still executed to fetch the resources from the App Store.
- (void) beginAccessingResourcesWithCompletionHandler:(void (^)(NSError *error)) completionHandler
The snippet used to fetch the book covers in the example Library-app is as follows;
[self.request conditionallyBeginAccessingResourcesWithCompletionHandler:^(BOOL resourcesAvailable)
{
if(resourcesAvailable == YES) {
[coverImage setImage:[UIImage imageNamed:[NSString stringWithFormat:@”%@”,self.book.title]]];
}
else {
[self.request beginAccessingResourcesWithCompletionHandler:^(NSError * _Nullable error) {
if(error) {
//handle error here
}
else {
[coverImage setImage:[UIImage imageNamed:[NSString stringWithFormat:@”%@”,self.book.title]]];
}
}];
}
}];
You’ll require some degree of control over the way your resources are downloaded when using On-Demand Resources, and perform modifications or scale based on the information you already have. NSBundleResourceRequest provides two ways to control and keep track of your resources;
- Download Priorities Set the download priority of your request using loadingPriority based on the need with a value between 0.0 and 1.0. The user using the Library-app should be able to read the book’s information right away, while the high-resolution cover can wait for a little longer. If the need is critical, use NSBundleResourceRequestLoadingPriorityUrgent as the priority.
- Download Progress User key-value observing to observe fractionCompleted on the progress property of your request object. Monitoring the progress of your request’s download is vital to the user’s experience with the app, allowing you to control what the user sees by blocking the view until it is ready, and preempt the user’s actions with relevant information and provide the best experience possible.
In all scenarios, the request has to end with you getting the resources for the tags you’ve specified, but if the user navigates away from the intended flow or the scenario changes, you can use the following methods to
pause
[self.request.progress pause];
resume
[self.request.progress resume];
or cancel the request;
[self.request.progress cancel];
When the resources are no longer needed, for example when you’re navigating away from a view that no longer relies on the covers tag, call
- (void)endAccessingResources
to inform the system that you’re done using the resources assigned with the tag. Using endAccessingResources updates the status of a request to not-needed, at which point the system begins purging them from the local store.
However, if you’re going to be using the resources from the tag at a later point and do not want it purged from the device’s storage, use setPreservationPriority:forTags: to set the priority for tags, where the priority is a value between a minimum of 0.0 and a maximum priority of 1.0. This enforces a sense of longevity to the resources attached to a tag, and leaves it on your device for a period of time indicated by the preservation priority.
Downloading and managing tags are done autonomously, but keeping the user waiting is second only to app crashes when it comes to UX sins. Avoiding this entire awkward situation is far more efficient than actually accounting for it, and there’s a whole bunch of things you can do together to make this happen.
Here’s a few guidelines to keep things smooth and sexy;
- Download assets at an appropriately early stage so that the user’s interaction flow isn’t interrupted. This comes with rigorous testing, to be honest, and there’s no easy way to do this.
- Package your assets well, and account for the potential asset size. Modularity is everything, and might make all the difference.
- Take into account the kind of internet connection the user is on before you download assets, and display warnings or alerts when required. Any app that chews through precious cellular data is gonna get uninstalled faster than Hola VPN.
- Hope for the best, but prepare for the worst.
- Use placeholders wherever necessary while resources are being loaded. Nobody wants to see a blank screen, or even worse, believe that the app is frozen or buggy. Create waypoints while building the architecture and plan aggressively. Then plan some more.
Rounding this off, here’s a bit of information that may come in handy. You can host your On-Demand Resources on a web server that complies with Apple’s hosting requirements.
Building apps that provide an excellent user experience while making the lives of developers easy are hard to come by. Taking advantage of App Thinning and newer optimization techniques is sure to raise your iOS game to the next level.
Here’s a link to the GitHub repo containing the example application used in this article.
Originally published on Mutual Mobile, “App Thinning in iOS 9 & NSBundleResourceRequest.”