Providing XCAssets Folder in Swift Package Xcode 12

Hello, I am struggling with providing assets via Swift Package Manager and an assets folder.

I have a swift package "A" that has an assets folder with a "image". I am able to build a preview and see the image in SPM "A". 👍🏻

Now I have a swift package "B" that depends on "A". In "B" I would like to have access of the "image". When I build and preview in "B" I get the following error: "Fatal error: unable to find bundle named [redacted]: file [redacted]/resource_bundle_accessor.swift", line 27" 😢 please help.

I am exposing the image via a public var that returns Image("image", bundle: .module)

Replies

I have the same issue too。Hope apple engineer can help~
On Xcode 12.1, SwiftUI Previewer crashes when it references another swift package it depends on and that swift package uses Bundle.module.

Basic code snippet that crashes:

Code Block
import SwiftUI
import Theme // Import package that uses Bundle.module
/// This view will crash the previewer
struct ThisViewCrashesPreview: View {
var body: some View {
Text("Hello, World!")
// If you comment out the following line, the previewer will render
.foregroundColor(Color.themeGreenFromXCAssets)
// Or use this line, the preview will also start to work
.foregroundColor(Color.colorThatDoesNotUseModuleReference)
}
}
struct ThisViewCrashesPreview_Previews: PreviewProvider {
static var previews: some View {
ThisViewCrashesPreview()
}
}


Code from Theme package:

Code Block
import SwiftUI
public extension Color {
static let themeGreenFromXCAssets = Color("ThemeGreen", bundle: .module)
static let colorThatDoesNotUseModuleReference = Color.init(red: 0.3, green: 0.6, blue: 0.9)
}


Crash Log States:

Application Specific Information:
Fatal error: unable to find bundle named ThemeTheme: file Theme/resourcebundle_accessor.swift, line 27

Same issue reported here: https://stackoverflow.com/questions/64540082/xcode-12-swiftui-preview-doesnt-work-on-swift-package-when-have-another-swift/64664692#64664692

To reproduce:
  1. Download code at: https://github.com/ryanholden8?tab=repositories See repo SwiftUI-Preview-Failing-Test-Project

  2. Open PreviewFailingTestProject.xcodeproj

  3. Change Target at the top of the Xcode window from PreviewFailingTestProject to MyUICode

  4. Change deployment device to iPhone 12 mini (or any iOS device)

  5. Open file: LocalPackages > MyUICode > Sources > MyUICode > ThisViewCrashesPreview.swift

  6. Try to use the SwiftUI Previewer

  7. Crash will occur and preview will not render, see lines 8-11 of ThisViewCrashesPreview.swift for more details

  8. NOTE: See file: PreviewFailingTestProject > PreviewFailingTestProjectApp.swift > MainWindow View on lines 13-19. This renders fine on both the SwiftUI Previewer and when deployed to device.

Anyone know of a work around for this?
SwiftUi previews works for me using images in a XCAssets folder within my package - provided I reference the bundle appropriately within the module.

In the package, consider using something like:

Code Block Swift
public struct MyStruct {
...
public var image: Image {
    return Image(imageName, bundle: Bundle.module)
}
}

where imageName is the String name of the image in the XCAssets folder.

Note the use of
Code Block Swift
Bundle.module


Then in the project:
Code Block Swift
import SwiftUI
import MyPackage
let data: MyStruct
...
var body: some View {
VStack {
...
data.image
.resizable()
.cornerRadius(8)
....
...
}
}


We found a workaround for the situation described by @iRyan8, which is that the linked local package (i.e. Theme) is unable to resolve its own bundle because the paths used in the resource_bundle_accessor.swift implementation don't include the path to the bundle when it is linked via Preview.

Our "solution" is to write a replacement for resource_bundle_accessor.swift that includes a 4th path (line 17):

Code Block Swift
extension Foundation.Bundle {
    static var myModule: Bundle = {
/* The name of your local package, prepended by "LocalPackages_" */
        let bundleName = "LocalPackages_Theme"
        let candidates = [
            /* Bundle should be present here when the package is linked into an App. */
            Bundle.main.resourceURL,
            /* Bundle should be present here when the package is linked into a framework. */
            Bundle(for: CurrentBundleFinder.self).resourceURL,
            /* For command-line tools. */
            Bundle.main.bundleURL,
            /* Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/"). */
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
        ]
        for candidate in candidates {
            let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
            if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("unable to find bundle named \(bundleName)")
    }()
}


To use this, replace references to Bundle.module with Bundle.myModule, like so:
Code Block Swift
/* Replace this... */
static let themeGreenFromXCAssets = Color("ThemeGreen", bundle: .module)
/* With this: */
static let themeGreenFromXCAssets = Color("ThemeGreen", bundle: .myModule)


Hopefully this will be addressed by an update soon. The above workaround is at least applicable for Xcode 12.3.
I just noticed that I neglected to include the definition for CurrentBundleFinder in my previous reply. It can be any class, as long as it is defined in the module of the bundle you're trying to access. For example, the following is sufficient:
Code Block Swift
class CurrentBundleFinder {}

Updated workaround for both iOS and macOS (not tested with Catalyst) based on Skyler_s suggestion

Code Block swift
public let imageBundle = Bundle.myModule
private class CurrentBundleFinder {}
extension Foundation.Bundle {
    static var myModule: Bundle = {
        /* The name of your local package, prepended by "LocalPackages_" for iOS and "PackageName_" for macOS. You may have same PackageName and TargetName*/
        let bundleNameIOS = "LocalPackages_TargetName"
        let bundleNameMacOs = "PackageName_TargetName"
        let candidates = [
            /* Bundle should be present here when the package is linked into an App. */
            Bundle.main.resourceURL,
            /* Bundle should be present here when the package is linked into a framework. */
            Bundle(for: CurrentBundleFinder.self).resourceURL,
            /* For command-line tools. */
            Bundle.main.bundleURL,
            /* Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/"). */
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(),
            Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
        ]
        
        for candidate in candidates {
            let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle")
            let bundlePathMacOS = candidate?.appendingPathComponent(bundleNameMacOs + ".bundle")
            if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) {
                return bundle
            } else if let bundle = bundlePathMacOS.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("unable to find bundle")
    }()
}

Initially I had posted that the workaround didn't work, but I had put the following variables with the wrong names

Code Block
let bundleNameIOS = "LocalPackages_TargetName"
let bundleNameMacOs = "PackageName_TargetName"

And I had forgotten to declare the resources in Package.swift

Code Block
resources: [.process("Resources")]


I confirmed that it is working on XCode 12.4 and XCode 12.5 Beta

Thanks!
In the following code

Code Block swift
let bundleNameIOS = "LocalPackages_TargetName"
let bundleNameMacOs = "PackageName_TargetName"


what are we supposed to replace "LocalPackages" with? Or is this a literal and not a placeholder?

Using the workaround suggested by Skyler_S and Nekitosss works! The only scenario in which it doesn't is when you are using the bundle in UI Tests. I've found that it's sufficient to add:

Code Block swift
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent()

to the list of candidates:

Code Block swift
let candidates = [
/* Bundle should be present here when the package is linked into an App. */
Bundle.main.resourceURL,
/* Bundle should be present here when the package is linked into a framework. */
Bundle(for: CurrentBundleFinder.self).resourceURL,
/* Bundle should be present here when the package is used in UI Tests. */
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent(),
/* For command-line tools. */
Bundle.main.bundleURL,
/* Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/"). */
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(),
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent()
]