How to Use Module Resources in Objective-C SPM Packages

18 August 2020

With the release of Swift 5.3, Swift Package Manager took a big step towards being able to replace legacy dependency managers like Carthage and CocoaPods. As detailed in WWDC Session 10169 Swift Packages now support the bundling of resource files within a module. What's more, as this is a new compiler feature we don't have to wait to drop pre-iOS 14 support to take advantage of it - we can begin using it as soon as we start building our apps with Xcode 12. However, at first glance it may appear that Apple has neglected to add support for module resources in Objective-C package targets, preventing us from adding SPM support to older framework projects. Fortunately this is not the case, and resource bundles are fully supported for Objective-C targets.

Adding Resources to a Module

Adding resources to an SPM module is a simple enough process. By adding files that are not source code to the path directory of a Package's Target (or one of its child directories), Xcode automatically associates the files with the relevant Target. Helpfully, common resource types are automatically detected and added to the Target's resources. According to Apple, these common resource types are:

Notably missing from this list is .plist files, which were commonly used as resources in Objective-C frameworks. Any files not automatically recognised as bundle resources by Xcode will cause an error when building the package. These files will need to be explicitly included in or excluded from the Target, which we can specify in the Package manifest:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ExamplePackage",
    products: [
        .library(name: "ExamplePackageLib", targets: ["ExampleTarget"])
    ],
    targets: [
        .target(name: "ExampleTarget", resources: [.copy("Data.plist")])
    ]
)

Note the Data.plist resource in the ExampleTarget Target, which will be copied in to the resource bundle of the Target.

How Is the Resource Bundle Generated?

The resources of our Target are packaged up as part of the build process in to a Resource Bundle. To ensure that the resources of multiple Targets are not intermingled, each resource bundle is given a unique name derived from the Package and Target names. For example, our manifest above would produce a bundle named ExamplePackage_ExampleTarget​.bundle. This bundle will contain the Data.plist file that we specified, along with any other resources automatically detected by Xcode.

Accessing Module Resources in Swift Packages

Now that we have our resources identified and bundled for us, how do we access them in our framework's code? When a Target is built using Swift source code, if the Target produces a resource bundle then an extension is added to Bundle that adds a static property named module. Accessing this property returns the resource bundle produced as part of the Target's build. For example, accessing an image in our Module is as simple as:

let image = UIImage(named: "ImageName", in: Bundle.module, compatibleWith: nil)

You may wonder where this module property comes from, particularly given that it is not included in the Bundle class documentation. In fact, it is generated by SPM during the build process - an extension specific to your Target is created and included in the build. If you wish to see this file, it can be found in the DerivedSources directory inside your package's DerivedData folder1. It is named resource_bundle_accessor.swift and looks like this:

import class Foundation.Bundle

private class BundleFinder {}

extension Foundation.Bundle {
    /// Returns the resource bundle associated with the current Swift module.
    static var module: Bundle = {
        let bundleName = "ExamplePackage_ExampleTarget"

        let candidates = [
            // Bundle should be present here when the package is linked into an App.
            Bundle.main.resourceURL,

            // Bundle should be present here when the package is linked into a framework.
            Bundle(for: BundleFinder.self).resourceURL,

            // For command-line tools.
            Bundle.main.bundleURL,
        ]

        for candidate in candidates {
            let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
            if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("unable to find bundle named ExamplePackage_ExampleTarget")
    }()
}

As you can see, it uses the <ModuleName>_<TargetName>.bundle naming structure discussed earlier, and uses a variety of bundle locations to search for the generated bundle, ensuring that your resources can be located irrespective of how the module is used in practice.

Accessing Module Resources in Objective-C Packages

Due to the way that module resource bundles are accessed in Swift, you may have assumed that in Objective-C Targets there would be a similar accessor generated on NSBundle. But you'd be wrong! Instead, the accessor is produced as a static function name-spaced to the individual Target's bundle, rather than an easily identifiable property on NSBundle. It too can be found inside the DerivedSources folder of the Target, and the header file named resource_bundle_accessor.h is as follows:

#import <Foundation/Foundation.h>

NSBundle* EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLE(void);

#define SWIFTPM_MODULE_BUNDLE EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLE()

As you can see, this generated file also contains a handy preprocessor directive named SWIFTPM_MODULE_BUNDLE, which you can use in your Target's source code to refer to the generated bundle. The corresponding resource_bundle_accessor.m is as follows:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLER_FINDER : NSObject
@end

@implementation EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLER_FINDER
@end

NSBundle* EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLE() {
    NSString *bundleName = @"ExamplePackage_ExampleTarget";

    NSArray<NSURL*> *candidates = @[
        NSBundle.mainBundle.resourceURL,
        [NSBundle bundleForClass:[EXAMPLEPACKAGE_EXAMPLETARGET_SWIFTPM_MODULE_BUNDLER_FINDER class]].resourceURL,
        NSBundle.mainBundle.bundleURL
    ];
    
    for (NSURL* candiate in candidates) {
        NSURL *bundlePath = [candiate URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.bundle", bundleName]];
    
        NSBundle *bundle = [NSBundle bundleWithURL:bundlePath];
        if (bundle != nil) {
            return bundle;
        }
    }
    
    @throw [[NSException alloc] initWithName:@"SwiftPMResourcesAccessor" reason:[NSString stringWithFormat:@"unable to find bundle named %@", bundleName] userInfo:nil];
}

NS_ASSUME_NONNULL_END

As you can see, this is doing exactly the same thing as the Swift generated accessor, only with the addition of an explicitly name-spaced bundle class as Objective-C classes are part of a global namespace that would clash if more than one module used the same class name to access its resource bundle.

Usage of the resource bundle, as in Swift, is similarly simple:

UIImage *image = [UIImage imageNamed:@"ImageName" inBundle:SWIFTPM_MODULE_BUNDLE withConfiguration:nil];

Conclusion

Making use of SPM package resources is easy in both Swift and Objective-C modules. For many, this removes the last barrier to supporting Swift Package Manager as an option for module integration in to application projects. If your app relies on frameworks that you have source control over, now is the perfect time to see whether they can be upgraded to support building with SPM. Once they and any 3rd party dependencies add this support, you will be able to benefit from a 1st party dependency management system built right in to Xcode, and fighting with 3rd party dependency managers will become a thing of the past.

Further Reading

For more information on resource bundling, see Bundling Resources with a Swift Package.

Footnotes

1: The exact location of the DerivedSources folder is DerivedData/​ProjectName-<random letters>/​Build/​Intermediates.noindex/​PackageName.build/​<target-platform>/​TargetName.build/​DerivedSources