The Ins and Outs of Getting the Bundle
Background
Apple uses bundles to represent apps, frameworks, plug-ins, and many other specific types of content and we often need to access to the resources like image assets, localised string resources or other types of files that embedded in these bundles, for which, we need to get hold of the bundle objects.
For many cases, we do not need to worry about it as Apple provided APIs to access these resources easily. For example, we can use the following syntax to create a UIImage object, Image object and a localised string.
let uikitImage = UIImage(named: "image")
let swiftUIimage = Image(name: "image")
let string = String(localized: "Localised string")
In all the cases above, the methods seek the resources in the default bundle. For the cases that resources are not stored in the default bundle, we need to use their full-fledged version like below, where you can see we are able to specify which bundle object to use:
UIImage?(named name: String,
in bundle: Bundle?,
compatibleWith traitCollection: UITraitCollection?)
Image(_ name: String, bundle: Bundle? = nil)
String(localized keyAndValue: String.LocalizationValue,
table: String? = nil,
bundle: Bundle? = nil,
locale: Locale = .current,
comment: StaticString? = nil)
So, the question is, when needed, how we choose the best methods to get the bundle objects.
Different ways of getting bundle object
Class variable — main
This returns the bundle object that contains the current executable, which is the bundle object used in the methods that do not require bundle parameter.
Pay attention to the current executable here. If we have a localised Localizable.xcstrings resources embed in the current executable bundle, this is the bundle object that need to be used and the app target is one the types of the executables.
What if it is a unit test target? It turns about this method does not work even if we embed the resource files into it. It does not work for UI testing target too. In summary, main is never the testing target.
Just in case you may wonder why we embed some resource file into Unit testing targets or UI testing targets. From my previous experience, sometimes, we want to share some configuration data among those targets in order to avoid duplicating them.
init(for: AnyClass)
This returns the bundle object with which the specified class is associated. For example, if we define the BundleFinder class and call this method like below:
private class BundleFinder {}
let bundle = Bundle(for: BundleFinder.self)
The bundle will always be the target that includes the definition of this BundleFinder class even if it is a unit testing target or UI testing target, which means if we want to access the resources that embedded in the testing targets, this would be one the possible options.
init?(identifier: String)
This method returns the bundle with the specified identifier if it can find. Similarly init?(url: URL) and init?(path: String) return the bundle with specified URL or path string.
You may quickly realise that we could use this method if we want to access the resources that embed in the app target from Unit testing target or UI testing target. It works indeed. And, from my point of view, it is the great way to do so. With little cost of specifying identifier using this method, we don’t need to bother setting up the target membership for our resources files.
Class variable — module
This is the way we access to the resources in Swift packages. Once we include resources in a package, the following code will be generated(the bundleName in the code will vary) and we can use module to access the package bundle.
import class Foundation.Bundle
import class Foundation.ProcessInfo
import struct Foundation.URL
private class BundleFinder {}
extension Foundation.Bundle {
/// Returns the resource bundle associated with the current Swift module.
static let module: Bundle = {
let bundleName = "MyLibrary_SampleLibrary"
let overrides: \[URL\]
#if DEBUG
// The 'PACKAGE\_RESOURCE\_BUNDLE\_PATH' name is preferred since the expected value is a path. The
// check for 'PACKAGE\_RESOURCE\_BUNDLE\_URL' will be removed when all clients have switched over.
// This removal is tracked by rdar://107766372.
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"]
?? ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] {
overrides = [URL(fileURLWithPath: override)]
} else {
overrides = []
}
#else
overrides = []
#endif
let candidates = overrides + [
// 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 MyLibrary_MyLibrary")
}()
}
Please note, this is the only recommended way to access resources within Swift packages. Out of curiosity, we can try init(for: AnyClass) method. It did not work. However, init?(identifier: String) works. The only thing for it to work is that we need to know what’s the identifier for our package, for which, we can run the following code in the target that is using the package to find out.
The Swift package identifier is in the format like PackageName-ModuleName-resources at the time when I tried out. But it is not guaranteed that it will never change. So it’s not recommended to use.
Bundle.allBundles.forEach {
print($0.bundleIdentifier ?? "")
}
Because of the fact that we need to access resources relying on the Bundle.module variable in Swift packages and this variable only exists in Swift packages, if we have shared code between packages and Xcode projects, we need a way to make it work for both setups. Luckily, Swift package has a preprocessor definition SWIFT_PACKAGE for this purpose and we can use it like below so that we can use bundle property in other part of the code without worrying how to get the right bundle object.
private class BundleFinder {}
var bundle: Bundle {
#if SWIFT_PACKAGE
Bundle.module
#else
Bundle(for: BundleFinder.self)
#endif
}
Conclusion
In this article, we have gone through different ways of getting bundle objects. Different methods have different use cases and require different setups of the resources(for example, the target membership setting for resources). Hope this will help you find the best way for your use cases.
Sharing is caring!