Ace Green
Ace Green

Reputation: 391

Swift Package Manager - Storyboard bundle

I'm trying to add support for SPM in one of our projects that has storyboards.

Currently we grab it UIStoryboard(name: String, bundle: String?) but this doesn't seem to work with SPM, as there isn't really a bundle. Even printing all the bundles doesn't show the bundle of our package.

Any way we can support storyboards or are SPM's meant to be just files?

Attempts:

UIStoryboard(name: "GiftCards", bundle: Bundle(for: self))
UIStoryboard(name: "GiftCards", bundle: Bundle(for: type(of: self)))
UIStoryboard(name: "GiftCards", bundle: Bundle(identifier: "com.x.x"))

Upvotes: 15

Views: 6005

Answers (6)

Miro Markaravanes
Miro Markaravanes

Reputation: 3367

Solution for those trying with iOS 18/Xcode 16. Here's what worked:

@mm2001's answer is mostly correct and still required as of Xcode 16 for referencing storyboards from SPM packages inside a parent storyboard. @derpoliuk's answer only applies if you are instantiating the storyboard from the package itself.

However, the bundle name for configuring the reference inside the parent storyboard does not seem to be correct in @mm2001's answer. The correct bundle identifier format FOR XCODE 16 onward is as follows: lowercasepackage.PascalCasePackage.resources. For BadgeKit, it should be badgekit.BadgeKit.resources. The old bundle identifier format is still valid and working for Xcode 15.4.

In short:

  1. Make sure the SPM package copies the storyboard by using the resources option in the library's Package.swift.
    ....
    targets: [
        .target(name: "BadgeKit",
                ...
                resources: [
                    .copy("Views/BadgeKit.storyboard"),
                ],
                ...
        ),
    ]
    ....
  1. Add SPM package as dependency.
  2. Add storyboard reference inside the parent storyboard and make sure to set bundle identifier with format lowercasepackage.PascalCasePackage.resources. Ex. For BadgeKit, it should be badgekit.BadgeKit.resources.
  3. Add the loader code inside AppDelegate.
@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()
        }

        […]
    }

    […]

}

Upvotes: 2

mm2001
mm2001

Reputation: 6969

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"

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”

Storyboard property panel with Module value BadgeKit

Upvotes: 14

Badr Bujbara
Badr Bujbara

Reputation: 8671

the key thing is to use Bundle.module when instantiating the storyboard

1- Add this view controller extension to the swift package:

 public extension UIViewController{
        
        public static func getStoryboardVC() -> UIViewController { 
            let storyboard = UIStoryboard(name: String(describing: self), bundle: Bundle.module) // Use Bundle.module
            return storyboard.instantiateInitialViewController()!
        }
 }

The Bundle.module represents the containing package.

2- In the app, in my case the swift package is called MySwiftPackage. I call that extension method from the swift package to instantiate the view controller I want to present:

   @IBAction func openCard(){
        let vc = MySwiftPackage.MyViewController.getStoryboardVC() as! MySwiftPackage.MyViewController
        vc.personNo = "11111"
        vc.personId = "8888888"
        present(vc, animated: true, completion: nil)
    }

Upvotes: 11

derpoliuk
derpoliuk

Reputation: 1816

For Xcode 13 both top-rated answers are partially helpful (first and second), so I decided to put summary together.

To make storyboards work in Swift Package, you need to:

  1. In your storyboard manually select Module for your view controller:

    View Controller's Module

  2. Pass Bundle.module when creating a storyboard:

    let storyboard = UIStoryboard(name: "ViewController", bundle: Bundle.module)
    return storyboard.instantiateInitialViewController() as! ViewController
    

Example:

I created a simple example for this on GitHub: https://github.com/derpoliuk/swift-module-storyboard

Upvotes: 5

Wendy Liga
Wendy Liga

Reputation: 740

starting on Swift 5.3, thanks to SE-0271, you can add bundle resources on swift package manager by adding resources on your .target declaration.

example:

.target(
   name: "HelloWorldProgram",
   dependencies: [], 
   resources: [.process(Images), .process("README.md")]
)

if you want to learn more, I have written an article on medium, discussing this topic

Upvotes: 3

bscothern
bscothern

Reputation: 2014

Resources are not currently supported with SwiftPM. There is a proposal in the works here.

Upvotes: 0

Related Questions