Categories
Open Source

Adding Resources to a Swift Package Quickly (and Dirty)

As you may heard, Swift packages in Xcode 12 (i.e. Swift 5.3) can (finally!) ship with resources and binary dependencies. This makes Swift Package Manager more or less equal to other dependency management systems, such as CocoaPods, in terms of features.

My Space Photos app (which I wrote about previously) relies on a library called SKPhotoBrowser for presenting a full-screen image viewer. The original library hasn’t been updated in over a year, and it obviously doesn’t support resources in SPM. (Furthermore, it’s not available via SPM altogether: likely due to the lack of resources support). At the same time, I have a goal of gradually transitioning Space Photos to SPM entirely. (At the moment, it mixes CocoaPods’s pods with SPM packages.)

These two things considered, I decided to fork SKPhotoBrowser and update it with SPM support.

I should disclose right away mine was not a perfect solution, and I wouldn’t be proud to contribute it back. In my commits, I introduced breaking changes (which arguably were inevitable) and also dropped support for CocoaPods (which surely could’ve been avoided). But since I needed to solve one specific problem—enable SPM support—and didn’t care about CocoaPods compatibility much, it worked for me and my app. Maybe one day I’ll revisit the fork and make amends…


The actual steps for adding Swift Package Manager support to an existing library are quite straightforward. If you’ve done it before, you wouldn’t discover anything new this time; if you haven’t, the official guide kinda covers the topic from top to bottom.

Here’s the resulting Package.swift manifest file. I basically just copied all the details from the .podspec.

// swift-tools-version:5.3

import PackageDescription

let package = Package(
    name: "SKPhotoBrowser",
    platforms: [
        .iOS(.v8)
    ],
    products: [
        .library(
            name: "SKPhotoBrowser",
            targets: ["SKPhotoBrowser"]
        ),
    ],
    targets: [
        .target(
            name: "SKPhotoBrowser",
            dependencies: [],
            path: "SKPhotoBrowser"
        ),
        .testTarget(
            name: "SKPhotoBrowserTests",
            dependencies: ["SKPhotoBrowser"],
            path: "SKPhotoBrowserTests"
        ),
    ]
)

You may notice this package declaration is labeled with swift-tools-version:5.3. In case you haven’t worked with SPM much, it’s the minimum Swift version required to build the package. Nevertheless, the code above is completely identical to what you would’ve gotten in a previous version—and just as valid. Now you might be wondering, and what about resources? how will SPM find them?

Let me tell you about a little trick. SPM will discover common resource types automatically, as long as they’re kept inside the folder for the target they belong to. Apple’s documentation says the following kinds of resources can be discovered automatically:

  • Interface Builder files (NIBs and storyboards);
  • Core Data resources (e.g. .xcdatamodeld files);
  • Asset catalogs (.xcassets);
  • .lproj folders with localized resources.

You can include resources of any other kind, though in that case you’ll have to specify their location explicitly (see documentation for Target).

Before I made changes to SKPhotoBrowser, its resources had been enclosed in a .bundle which CocoaPods had embedded as a separate target. I decided not to spend too much time trying to preserve backward compatibility with CocoaPods, so I got rid of the .bundle thing and moved its contents to an asset catalog (Images.xcassets). Not only doing so made the images eligible for auto discovery, but also enabled image management and optimization capabilities of Xcode asset catalogs.

Here’s another trick. Swift Package Manager lets you know whether resources have been discovered and are available, before you even build the package, let alone install it as another product’s dependency. If you did everything correctly, a static property module will be available on Bundle. This property (Bundle.module) is how you access resources from within the package. Here’s an example:

let image = UIImage(named: "my_image", in: Bundle.module, compatibleWith: nil)

That’s it! No kidding.

As mentioned above, I removed CocoaPods support in my fork of SKPhotoBrowser as it no longer functioned. But basically, that’s all the changes made. (I also incremented major version number, as per SemVer.) As a matter of fact, the fork version won’t be merged back; not until CocoaPods support is restored.

If you’d like, compare my fork with the original to see how little it took to add support for SPM and enable resources. You can also check out the result in a project or package of your own:

.package(url: "https://github.com/yakovmanshin/SKPhotoBrowser.git", from: "7.0.0")

To be honest, I was greatly surprised when I learned how effortless adding resources to Swift packages is. I was prepared to find myself in the middle of a command line mess similar to that of generating XCFrameworks.

Another good thing is, the Swift packages infrastructure has become just as good as its longstanding alternatives (competitors?), CocoaPods and Carthage. With support for resources, localization, and binary dependencies, Swift Package Manager can now fulfill nearly any purpose.