Swift Package Manager - Storyboard bundle
Asked Answered
A

6

15

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"))
Assailant answered 18/9, 2019 at 20:13 Comment(0)
F
11

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)
    }
Frangible answered 20/1, 2021 at 8:33 Comment(1)
Yup, this will work too! We chose to keep the code changes isolated to the main app, in the initializer of the 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
L
14

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

Lindholm answered 22/9, 2020 at 8:39 Comment(8)
omg, it worked! only let _= in the app delegate should be let bundle = How did you know this stuff???Frangible
Thanks for that correction @fullmoon. Typo is now fixed. Full credit to user salutis for working this out originally.Lindholm
Awesome! actually it also worked using another way. Key thing is using Bundle.module in the package to instantiate the storyboard view controller. I added a new answer if you'd like to check.Frangible
I get "Could not find a storyboard named" ... and it's still looking in my main .app not in the Bundle I specified... 😭🤔... I'm AM using the package target in other parts of the code....must be a timing issue..... Adding the workaround fixed it.Destitution
Any ideas why it stops working when the build is created for the App Store or as an .ipa file?Frangible
@fullmoon fwiw it is working ok for me in .ipa files and in the App Store. Do you have the loading workaround in the AppDelegate (as shown above)?Lindholm
@Lindholm Yes I implemented the workaround as well as the other way. Both work perfect when building from Xcode to device, but exporting as .ipa will generate this error: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'ARCameraVC' in bundle NSBundle </private/var/containers/Bundle/Application/.../MyIDCardAppClip.app> (loaded)' It didn't consider the let bundleNames = ["ARMap_ARMap"] I declared in the appDelegate following the workaround.Frangible
I solved it with the magic of starting fresh :) hehe. I was using the SP with an App Clip. All I did was I deleted the App Clip and created it again and linked the SP then it worked in .ipa and the App Store reviewers didn't complain.Frangible
F
11

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)
    }
Frangible answered 20/1, 2021 at 8:33 Comment(1)
Yup, this will work too! We chose to keep the code changes isolated to the main app, in the initializer of the 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
C
5

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

Counterweight answered 18/11, 2021 at 13:13 Comment(0)
S
3

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

Selfsealing answered 22/5, 2020 at 17:40 Comment(0)
U
0

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

Unaccountable answered 25/9, 2019 at 19:24 Comment(0)
D
0

Solution for those trying with iOS 18/Xcode 16 (or possibly even earlier versions). 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 name format is in identifier format as follows: lowercasepackage.PascalCasePackage.resources. For BadgeKit, it should be badgekit.BadgeKit.resources.

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()
        }

        […]
    }

    […]

}
Dagenham answered 14/8 at 11:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.