Loading Resources From A Framework

Small iOS applications are likely to contain the resources they need (images, localizations, audio files, etc.) directly in the application bundle. This keeps life simple until the day you decide to move some of the code and resources into a framework to reuse elsewhere.

In this post I look at a code snippet to load an image resource that will still work when moved to a framework and using type(of:) to find the type of a class at runtime (with a caveat).

Loading an Image

Suppose I have a custom view that loads an image from an asset catalog:

public class AmazingView: UIView {
  ...
  private func setupView() {
    guard let image = UIImage(named: "MyImage") else {
      fatalError("Missing MyImage...")
    }
    ...
  }
}

That works fine until one day I want to reuse my AmazingView in other applications and move it and its resources to a framework.

import AmazingFramework
...
let myView = AmazingView()

Unfortunately the application now crashes when creating the view with the message about the missing image. The problem is that UIImage(named:) assumes the asset (MyImage.png) is in the main application bundle. That is no longer true.

A Bundle (or NSBundle with Objective-C) is just a collection of resources. When you create an iOS application you start out with a main bundle (the contents of the .app file). When you create a framework Xcode stores the resources for that framework in a separate bundle (the file AmazingFramework.framework in this case).

To get the main bundle for an application:

// Swift
let mainBundle = Bundle.main
// Objective-C
NSBundle *mainBundle = [NSBundle mainBundle];

You can get the bundle that contains a class using init(for aClass: AnyClass). For the framework bundle that contains our AmazingView:

let amazingBundle = Bundle(for: AmazingView.self)

In Swift, using SomeClass.self returns the type (SomeClass.Type). For comparison you could write the same in Objective-C as follows:

NSBundle *amazingBundle = [NSBundle bundleForClass:[AmazingView class]];

To load an image from a framework bundle we need the longer UIImage initializer - init?(named:in:compatibileWith:). The second parameter is the name of the bundle containing the image file. Using nil is the same as passing the main bundle. So these all load an image from the main bundle:

UIImage(named: "MyImage", in: Bundle.main, compatibleWith: nil)
UIImage(named: "MyImage", in: nil, compatibleWith: nil)
UIImage(named: "MyImage")

To load our image from the framework bundle:

let amazingBundle = Bundle(for: AmazingView.self)
guard let image = UIImage(named: "MyImage", in: amazingBundle,
      compatibileWith: nil) else { ... }   

Getting the Type at Runtime

I don’t like writing the explicit class name (AmazingView.self) when getting the bundle. With Objective-C it is common to do this in a more generic way:

NSBundle *amazingBundle = [NSBundle bundleForClass:[self class]];

How do we do this with Swift? It turns out there is a way and like much of Swift at the moment that way changed recently. The Xcode 8 release notes mention the replacement of dynamicType with type(of:):

The dynamicType keyword has been removed from Swift. In its place a new primitive function type(of:) has been added to the language.

The Swift Standard Library Functions lists type(of:) as a global function. You pass it an instance of some type and you get back the type. For example:

let myView = AmazingView()
let typeOfClass = AmazingView.self    // AmazingView.Type
let typeOfInstance = type(of: myView) // AmazingView.Type

So to get the bundle we could write:

let bundle = Bundle(for: type(of: self))

One word of caution with this approach. Since type(of:) determines the type at runtime this can fail if the user of the framework subclasses my view (thanks to Douglas Hill for pointing this out). So unless the class is marked as final it is safer to stick with Bundle(for: AmazingView.self).

To summarise I ended up with this:

public class AmazingView: UIView {
  ...
  private func setupView() {
    let bundle = Bundle(for: AmazingView.self)
    guard let image = UIImage(named: "MyImage", in: bundle, compatibileWith: nil) else {
      fatalError("Missing MyImage...")
    }
    ...
  }
}