Bundles and asset catalogs are features of Apple’s systems every developer and app is using, even though many developers are probably not aware of their existence or how powerful they can be, especially when used together.

Today, I want to talk about what those two things are, what each one of them is supposed to do and how you can use both of them together to create a theming system for an app.

Bundles

Apple’s systems, including iOS, use bundles to represent a collection of resources organized in a directory structure. Your app is a bundle, every dynamic framework your app is linked to is also a bundle, storyboards are bundles, there are many examples of bundles used in iOS, macOS, tvOS and watchOS.

A bundle is basically just a folder with some extension, which is displayed by Finder as if it were a file. Application bundles use the well-known .app extension. You can inspect the contents of a bundle in Finder by right-clicking and selecting “Show Package Contents”.

Accessing a bundle’s resources

You’ve probably used the url(forResource:withExtension:) at some point while working on an app, the bundle you call this method on is usually the main bundle, represented by Bundle.main. When running on your app, Bundle.main will return your app’s bundle (the one that has the .app extension).

Accessing your app’s own resources is cool, but you can also access resources from other bundles. Let’s say your app has an embedded framework called Utilities and it has the bundle identifier codes.rambo.Utilities. Inside that framework, there’s an image file named image.png. To access that image from your app, you can instantiate a Bundle using the identifier codes.rambo.Utilities and then use url(forResource:withExtension:) to get the URL for the image file inside that bundle:

guard let bundle = Bundle(identifier: "codes.rambo.Utilities") else { return }
guard let imageURL = bundle.url(forResource: "image", withExtension: "png") else { return }

let image = UIImage(contentsOfFile: imageURL.path)

print(String(describing: image))

Custom bundles on iOS

So far I mentioned only standard bundles which can be created using one of Xcode’s built-in templates, but it’s also possible to create your own, custom bundles, you can even do that without using Xcode at all (more on this later).

Since there’s no Xcode template to create a custom bundle for iOS (only for the Mac), I decided to create my own. You can download the custom Xcode template here. To install the custom template, download it and run the following commands in Terminal:

cd ~/Downloads
unzip iOS_bundle_template.zip
mkdir -p ~/Library/Developer/Xcode/Templates/Custom
mv "iOS Resources Bundle.xctemplate" ~/Library/Developer/Xcode/Templates/Custom

Notice that the bundle template itself is a bundle, with the .xctemplate extension, how meta 😄. Restart Xcode to be able to access the new template.

Now that you have the custom template installed, with an iOS app project opened, you can go to File > New > Target and select iOS Resources Bundle.

On the next page, you can name the bundle, give it an identifier and optionally change the extension, or just leave the default extension which is .bundle.

That’s it, now you have a custom bundle you can use to store resources. My template is called iOS Resources Bundle because I made it to specifically store resources like images and sound, but bundles can also contain executable code.

Adding a resource to this bundle is just like adding a resource to any other target, drag the file into its group in Xcode and make sure the target membership is correct.

Another important step is to make sure your bundle is being built before the app that’s going to contain it and that it’s going to get copied into your app’s resources when the app is built.

That’s it, now you have a custom bundle you can use to store assets, it can be accessed like so:

guard let bundleURL = Bundle.main.url(forResource: "Pictures", withExtension: "bundle") else { return }
guard let bundle = Bundle(url: bundleURL) else { return }
guard let imageURL = bundle.url(forResource: "image", withExtension: "png") else { return }

let image = UIImage(contentsOfFile: imageURL.path)

print(String(describing: image))

The way you access the custom bundle is a little different from the framework because the custom bundle is not loaded by the runtime, so it can’t be looked up by identifier. To load it, we get a reference to its URL inside our app’s bundle, then initialize it with Bundle(url:). After the initialization, accessing its resources works the same way as it did for the framework bundle.

Asset Catalogs

Asset catalogs are a way to store app resources by mapping between named assets and files. Each asset can be represented by multiple files, each file targeting a specific set of device attributes such as device class, memory, version of metal and color gamut.

Most iOS developers are probably familiar with the usage of asset catalogs to store images such as icons, but asset catalogs can be used for more than just that. Since iOS 11, you can also store named colors in asset catalogs. Another lesser known feature of asset catalogs is that you can store arbitrary data assets, which can be literally anything.

Suppose you have a set of configurations for your app, but you need to change those configurations depending on how much memory the device has. You could do all of that in code, but using asset catalogs is easier. Take this configuration struct as an example:

struct Configuration: Codable {
  let numberOfParticles: Int
  let isLowEndDevice: Bool
  let enableShadowEffects: Bool
}

The above struct can be encoded as a property list for low end and high end devices:

Now that you have the configurations, you can add a new data asset to an asset catalog by selecting the + button in the asset catalog editor, adding a new data asset, checking the memory variations and dragging the high end plist to the 3 and 4 GB slots and the low end plist to the 1 and 2 GB slots:

To load a data asset, you use the class NSDataAsset:

let asset = NSDataAsset(name: "config")

To access the underlying data, you can use the data property of NSDataAsset, when you do that, the system is going to give you the correct data for the current device’s attributes automatically.

Note that all methods used to load resources from asset catalogs also have an optional bundle attribute you can provide in case you want to load assets from a bundle that’s not your app’s main bundle, like so:

let asset = NSDataAsset(name: "config", bundle: otherBundle)

Since Configuration is Codable, you can grab the property list from the asset catalog and use it to get the correct configuration for the current device. An extension on Configuration can make things easier for you:

extension Configuration {

  init?(assetNamed name: String, bundle: Bundle = .main) {
    guard let asset = NSDataAsset(name: name, bundle: bundle) else {
      return nil
    }
  
    guard let config = try? PropertyListDecoder().decode(Configuration.self, from: asset.data) else {
      return nil
    }

    self = config
  }

}

Then you can use the extension to load the configuration from your asset catalog:

let config = Configuration(assetNamed: "config")

Even better: instead of extending Configuration, you can extend the Decodable protocol itself so that any Decodable can be initialized from a data asset:

extension Decodable {

  init?(assetNamed name: String, bundle: Bundle = .main) {
    guard let asset = NSDataAsset(name: name, bundle: bundle) else {
      return nil
    }
  
    guard let instance = try? PropertyListDecoder().decode(Self.self, from: asset.data) else {
      return nil
    }

    self = instance
  }

}

This is a very simple example, but you can expand from here to take advantage of asset catalogs in your apps.

Case study: ChibiStudio

With the upcoming release of ChibiStudio 2.0, we’re moving the item packs to bundles and asset catalogs. A very important aspect of ChibiStudio since day one is that we want it to work without an internet connection and we want users to be able to use purchased item packs right away, without having to wait for a download to finish, this means we need to ship every single item pack with the app itself.

Currently, the packs are stored in three separate files/groups:

1 - The chibipackx file, a file containing metadata for the pack, such as its name and availability conditions (some packs are only available during a limited time period, such as the Easter pack).

2 - Several data files for each item in the pack, the files contain the Core Animation vector data used to draw the item and some metadata such as the color slots and which ones can be customized by the user.

3 - Several png files for each item in the pack, containing a small preview image of the item which is displayed in the grid. Drawing the vector representation for several items in a collection view is too expensive, so we need those previews.

With 2.0, this changes to:

1 - A bundle for each pack, the bundle contains an asset catalog which has an asset for each item’s vector data and another asset for each item’s preview image. The preview image is compressed using the new HEIF compression available on iOS 12, which makes the files smaller. We ended up not using the compression because of performance constraints.

2 - A database with metadata for all packs available and other metadata describing how the items and packs should be organized in the UI. All content is loaded from the corresponding pack bundles when needed, based on the metadata (each item has a unique identifier).

The new setup has several advantages over the previous one:

1 - Asset lookup doesn’t involve traversing directories and dealing with paths

2 - We can take advantage of the new compression available on iOS 12 and asset catalog thinning based on OS version and device We ended up not using the compression because it was too slow at runtime for smooth scrolling with hundreds of images in a collection view.

3 - The project itself doesn’t have to contain all of those files for each item (tens of thousands), improving build times

4 - Moving the metadata to a database gives us more flexibility as to how we choose to organize the items in the UI

This is only possible through the use of bundles. You can’t have multiple asset catalogs in a single bundle, if you create multiple catalogs in a single target in Xcode, they all get compiled to a single Assets.car file at the end. Separating the packs into their own bundles also makes it possible for us to add new features to packs in the future, such as metadata in the Info.plist file or even executable code.

Generating asset catalogs and bundles without Xcode

Packs for ChibiStudio are created in a custom Mac app we made specifically for this task. The artist draws the items in Sketch, exports them to the Chibi Pack Editor using a custom Sketch plugin, the editor then turns the Sketch drawings into Core Animation layers and assigns a category, index and layer for each item based on the layer name in Sketch. From there, the artist can customize aspects of the item such as the layer and color slots.

Since packs are created in the editor, it was necessary to add the asset catalog and bundle generator functionality to the app itself, the finished bundles are then imported into Xcode and added to the app’s resources in the build phases.

Asset catalogs created by Xcode have a specific directory structure and use JSON files to configure their contents. I’m not going to dive into too much detail on this, but you can read Apple’s documentation to understand the format.

The .xcassets folder created by Xcode is an editor representation of an asset catalog, which must be compiled in order to be used during runtime. Normally, Xcode compiles it for you, but you can also compile it manually using a command-line tool called actool.

To make the process easier in Chibi Pack Editor, I created a framework called AssetCatalogKit.

To generate bundles in the Editor, I created a template bundle which is embedded in the editor, after compiling the asset catalog for a pack, the editor copies this bundle template into the destination directory, moves the asset catalog into the bundle and creates its Info.plist file.

The result is a bundle with an Info.plist file and an Assets.car file, the bundle is located with the technique mentioned before, preview image assets are loaded using UIImage and vector data assets are loaded using NSDataAsset.

Practical example: theming

I didn’t want to end this article without providing some sample code for you to play with, so I made a very simple app that uses bundles for theming. The app has two bundles: Light.bundle and Dark.bundle. Each bundle has its own asset catalog with color definitions and a config asset containing configuration for that theme.

The app’s ThemeManager class takes care of loading the correct bundle for the selected theme and applying the colors and properties from that theme to the UI. You can find the sample app here.

This is a very simple example, with the same technique you could change more about your app depending on the theme, such as metrics (spacing, sizes, etc), images, or anything else that can be stored in an asset catalog or bundle.

In the sample app, I created the theme bundles using Xcode, but you could also create a simple Mac app as a theme editor for your iOS app and generate the bundles from that app, this app could then be given to your design team, giving them full flexibility when creating themes, without the need to install Xcode, deal with JSON or property lists and taking full advantage of asset catalogs.

This is precisely what we’re doing for ChibiStudio 2.0, which will also support theming, I created an app that allows me to define spatial metrics, font metrics and colors for different themes. The app exports a bundle for each theme and it also generates Swift code to make it easier to access the theme’s values. A theme can inherit values from other themes, similar to how CSS rules can be inherited.

I hope this article gave you some ideas of how to apply the power of bundles and asset catalogs on your projects, if you have any questions or comments, you can always reach out on Twitter.