Long title for something that should be simple, but actually kind of hard to search online.

Scenario/prerequisites:

  1. A Swift Package (library/command line) for iOS or macOS
  2. An image file (png or jpeg)
  3. SwiftUI :)

Step 1: Add resources to the Swift package

If your package doesn’t have any resources yet, follow these steps:

  1. Create a Resources folder inside your library code (eg. /Sources/MyLibrary/Resources)
  2. Drop your image(s) inside that folder
  3. In Package.swift, add the resources to your target:
     .target(name: "MyLibrary", dependencies: [], resources: [.process("Resources")]),
    

Step 2: Use the image in SwiftUI

It seems that, unfortunately, as of June 1 2021, with Xcode 12.5, SwiftUI still does not support loading image resources from a Swift Package bundle. This seems to work only with an Assets catalog in a normal Xcode project. Thus, the following examples won’t work:

var body: some View {
    Image("image").resizable()    // <-- this won't work ❌
    Image("image.png").resizable()    // <-- this won't work ❌
    Image("Resources/image.png").resizable()    // <-- this won't work ❌
    Image("image", bundle: Bundle.module).resizable()    // <-- this won't work ❌
    Image("image.png", bundle: Bundle.module).resizable()    // <-- this won't work ❌
}

Instead, we have to rely on UIImage for iOS and NSImage for macOS. Using Bundle.module we can retrieve the path of the image asset, and then load it with UIImage or NSImage as needed.

// UIKit
if let path = Bundle.module.path(forResource: name, ofType: type),
              let image = UIImage(contentsOfFile: path) {
  ...
}

// AppKit
if let path = Bundle.module.path(forResource: name, ofType: type),
              let image = NSImage(contentsOfFile: path) {
  ...
}

Step 3: Add an extension for Image

This extension for Image handles both AppKit and UIKit frameworks well, and also works well with SwiftUI previews 🎉:

extension Image {
    init(packageResource name: String, ofType type: String) {
        #if canImport(UIKit)
        guard let path = Bundle.module.path(forResource: name, ofType: type),
              let image = UIImage(contentsOfFile: path) else {
            self.init(name)
            return
        }
        self.init(uiImage: image)
        #elseif canImport(AppKit)
        guard let path = Bundle.module.path(forResource: name, ofType: type),
              let image = NSImage(contentsOfFile: path) else {
            self.init(name)
            return
        }
        self.init(nsImage: image)
        #else
        self.init(name)
        #endif
    }
}

It will return an empty image if the resource cannot be found or if neither UIKit or AppKit can be imported.

Step 4: Use the extension

We can then proceed to use this extension as follows:

var body: some View {
    Image(packageResource: "image", ofType: "png").resizable() // Works well ✅
}

Hope this helps!


This article was written as an issue on my Blog repository on GitHub (see Issue #28)

First draft: 2021-06-02

Published on: 2021-06-02

Last update: 2021-06-21