Use resource bundles in CocoaPods

10,717

Solution 1

If you use resource or resources in a CocoaPods PodSpec file, you tell Cocoapods that these are the resource files your library will load during runtime.

If your library is built as a dynamic framework, these files are just copied to the resource folder path of that framework and everything will be fine. Yet if your library is built as a static library, these are copied to the resource folder of the main application bundle (.app) and this can be a problem as this main application may already have a resource of that name or another Pod may have a resource of that name, in that case these files will overwrite each other. And whether a Pod is built as dynamic framework or as a static library is not specified by the PodSpec but in the Podfile by the application integrating your Pod.

Thus for Pods with resources, it is highly recommended to use resource_bundles instead!

In your case, the lines

s.resource_bundles = {
    'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}'] }

tell CocoaPods to create a resource bundle named MySDK (MySDK.bundle) and place all files matching the pattern into that resource bundle. If your Pod is built as a framework, this bundle is located in the resources folder of your framework bundle; if it is built as a static library, the bundle is copied to the resources folder of the main application bundle, which should be safe if you name your bundle the same way as your Pod (you should not name it "MySDK", rather "SchedJoulesSDK").

This bundle will have the same identifier as your Pod, however when dynamic frameworks are built, your framework bundle will have that identifier as well and then it's undefined behavior which bundle is being loaded when you load it by identifier (and currently the outer bundle always wins in my tests).

Correct code would look like this (not tested, though):

// Get the bundle containing the binary with the current class.
// If frameworks are used, this is the frameworks bundle (.framework),
// if static libraries are used, this is the main app bundle (.app).
let myBundle = Bundle(for: Self.self)

// Get the URL to the resource bundle within the bundle
// of the current class.
guard let resourceBundleURL = myBundle.url(
    forResource: "MySDK", withExtension: "bundle")
    else { fatalError("MySDK.bundle not found!") }

// Create a bundle object for the bundle found at that URL.
guard let resourceBundle = Bundle(url: resourceBundleURL)
    else { fatalError("Cannot access MySDK.bundle!") }

// Load your resources from this bundle.
let storyBoard = UIStoryboard(name: "SDK", bundle: resourceBundle)

As resourceBundle cannot change at runtime, it is safe to create it only once (e.g. on app start or when your framework is initialized) and store it into a global variable (or global class property), so you have it always around when needed (a bundle object also hardly uses any RAM memory, as it only encapsulates meta data):

final class SchedJoulesSDK {
    static let resourceBundle: Bundle = {
        let myBundle = Bundle(for: SchedJoulesSDK.self)

        guard let resourceBundleURL = myBundle.url(
            forResource: "MySDK", withExtension: "bundle")
            else { fatalError("MySDK.bundle not found!") }

        guard let resourceBundle = Bundle(url: resourceBundleURL)
            else { fatalError("Cannot access MySDK.bundle!") }

        return resourceBundle
    }()
}

The property is initialized lazy (that's default for static let properties, no need for the lazy keyword) and the system ensures that this happen only once, as a let property must not changed once initialized. Note that you cannot use Self.self in that context, you need to use the actual class name.

In your code you can now just use that bundle wherever needed:

let storyBoard = UIStoryboard(name: "SDK", 
    bundle: SchedJoulesSDK.resourceBundle)

Solution 2

You can use like...

s.resource  = "icon.png" //for single file

or

s.resources = "Resources/*.png" //for png file

or

s.resources = "Resources/**/*.{png,storyboard}" //for storyboard and png files

or

s.resource_bundles = {
   "<ResourceBundleName>" => ["path/to/resources/*/**"]
}

Solution 3

This is what I ended up with...

import Foundation

public extension Bundle {

    public static func resourceBundle(for frameworkClass: AnyClass) -> Bundle {
        guard let moduleName = String(reflecting: frameworkClass).components(separatedBy: ".").first else {
            fatalError("Couldn't determine module name from class \(frameworkClass)")
        }

        let frameworkBundle = Bundle(for: frameworkClass)

        guard let resourceBundleURL = frameworkBundle.url(forResource: moduleName, withExtension: "bundle"),
              let resourceBundle = Bundle(url: resourceBundleURL) else {
            fatalError("\(moduleName).bundle not found in \(frameworkBundle)")
        }

        return resourceBundle
    }
}

Example usage...

class MyViewController: UIViewController {
    init() {
        super.init(nibName: nil, bundle: Bundle.resourceBundle(for: Self.self))
    }
}

or

tableView.register(UINib(nibName: Self.loginStatusCellID, bundle: Bundle.resourceBundle(for: Self.self)), forCellReuseIdentifier: Self.loginStatusCellID)

Solution 4

I have the same issue, if I only use the bundle line:

    s.resource_bundles = {
        'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
    }

I get a runtime crash when I reference my storyboard. However, if I explicitly add each storyboard as a resource like so:

s.resources = ["Resources/MyStoryboard.storyboard", "Resources/MyStoryboard2.storyboard"]

Everything works fine. I don't think we should have to explicitly add each storyboard as a resource, but I haven't been able to make it work any other way.

One thing you could try is changing you resource bundle reference to recursively search in your folders with the ** nomenclature like this:

s.resource_bundles = {
    'MySDK' => ['SDK/**/*.{xib,storyboard,xcassets}']
}
Share:
10,717
Balázs Vincze
Author by

Balázs Vincze

Updated on June 18, 2022

Comments

  • Balázs Vincze
    Balázs Vincze almost 2 years

    I am making a pod (MySDK) and would like to load the assets from the separate resource bundles CocoaPods generates.

    However, I can not get it to work.

    Here is how I tried to load the storyboard:

    let storyBoard = UIStoryboard(name: "SDK", bundle: Bundle(identifier:"org.cocoapods.SchedJoulesSDK"))
    

    This gives the error:

    'Could not find a storyboard named 'SDK' in bundle

    The bundle is added in Xcode:

    And my podspec looks like this:

      s.resource_bundles = {
        'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
      }
    

    Any ideas?

  • Balázs Vincze
    Balázs Vincze almost 6 years
    How does this help me?
  • Denis Kutlubaev
    Denis Kutlubaev about 5 years
    I put XCAssets directory to my Pod and used third option. It worked!
  • Dipika
    Dipika almost 3 years
    Very well explained!! One question, same thing one would have to do for images and other resources as well. Right? If yes, then how to add images in Xibs inside the private pod project from a bundle to be known at runtime.
  • Mecki
    Mecki almost 3 years
    @Dipika It applies to all resources your Pod shall include, images, fonts, sounds, etc. As for referencing images within XIBs: If a XIB references an image by name, AFAIK the system will first try to find that image in the same bundle the XIB is located (either in that bundle resource folder or if it has an asset catalog, in that catalog). So as long as your images and XIBs are both in the same resource bundle, it should just work. Have you tried and it didn't? Then maybe you should start a new question on SO with a detailed sample.
  • Dipika
    Dipika almost 3 years
    I just tried it and it's working thanks😀