Update: XCode 14.2 (and perhaps earlier) does not require any of the steps below. Storyboards in the packages are automagically loaded as needed. Thanks to the answer by @derpoliuk for pointing this out and providing a GitHub example.
As of Xcode 12.0 this sort of works, but needs a few extra steps to complete it.
Scenario:
- an app that shows an embedded storyboard from a package named
BadgeKit
- a Swift package named
BadgeKit
with Package.swift
header // swift-tools-version:5.3
or higher
- a storyboard in BadgeKit called
BadgeKit.storyboard
Goal:
- Add a storyboard reference in an app storyboard and make it work in the app
Steps:
Add the storyboard reference to the app storyboard and configure it as follows:
Storyboard Reference property panel with Storyboard value BadgeKit
and Bundle identifier BadgeKit-BadgeKit-resources
.
Xcode automatically generates a bundle (and its identifier) for you to hold resources found in an SPM package using the following format: [package name]-[package target name]-resources
. In our case the package name and target name are the same (BadgeKit
).
While SPM resource bundles are always created and included in the app during the build process, they are not automatically available at runtime outside the package. If you aren't importing and using a package's target anywhere in your code, Xcode tries to optimize by not loading that package's resource bundle (it is probably an oversight on Apple's part that storyboard references alone aren't enough to trigger this). So a workaround is needed to trick Xcode into making an SPM package's bundle available if you are only using its resources in a storyboard.
Add this code to the app's AppDelegate.swift
file as a workaround:
@UIApplicationMain final class AppDelegate: UIResponder {
[…]
override init() {
super.init()
// WORKAROUND: Storyboards do not trigger the loading of resource bundles in Swift Packages.
let bundleNames = ["BadgeKit_BadgeKit"]
bundleNames.forEach { (bundleName) in
guard
let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"),
let bundle = Bundle(url: bundleURL) else {
preconditionFailure()
}
bundle.load()
}
[…]
}
[…]
}
In our example, the array bundleNames
contains a single string that correspond to the expected filename of the bundle our package will create for its resources during the build process. Xcode automatically names these bundle files as follows: [package name]_[package target name].bundle
. Note that a bundle's filename is not the same as its identifier.
If you are curious about which bundles (and their corresponding identifiers) are loaded and available at runtime, you can use the following code to troubleshoot:
let bundles = Bundle.allBundles
bundles.forEach { (bundle) in
print("Bundle identifier loaded: \(bundle.bundleIdentifier)") }
}
Configure the storyboard in the SPM BadgeKit package:
- Fill in “Module” with the SPM package target name ("BadgeKit")
- Uncheck “Inherit Module from Target”
AppDelegate
(my answer is now edited to show this). Hopefully Apple will improve this in the future and we can avoid either workaround. Thanks! – Lindholm