SwiftUI preview crashes when accessing assets from a nested Swift Package

Viewed 697

A Module

I have a module (A) that has two structs.

  • One with a color initiated directly in code.
  • One with a color loaded from the assets.
public struct CodeColor {
    public init() { }
    public let value = SwiftUI.Color(#colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1))
}

public struct AssetColor {
    public init() { }
    public let value = SwiftUI.Color("Legacy/Material/Gold", bundle: .module)
}

The preview is working like a charm:

A preview


B Module

The second module (B) should use the previous one (A) as a dependency to load colors from:

import A

public struct CodeColor {
    public init() { }
    public var value: SwiftUI.Color { A.CodeColor().value }
}

public struct AssetColor {
    public init() { }
    public var value: SwiftUI.Color { A.AssetColor().value }
}

But as soon as it touches the assets from module (A), the preview crashes:

B Preview

The error:

Can not preview in this file. Failed to update preview.

RemoteHumanReadableError: Failed to update preview.

The preview process appears to have crashed.

Error encountered when sending 'previewInstances' message to agent.

==================================

|  RemoteHumanReadableError: The operation couldn’t be completed. (BSServiceConnectionErrorDomain error 3.)
|  
|  BSServiceConnectionErrorDomain (3):
|  ==BSErrorCodeDescription: OperationFailed

So why is that?

Note: The stranger thing is that the exact B preview code is working if it is in an actual app (not another package)

Here is the full code on github

1 Answers

I found a solution in the developer forum:

Link

https://developer.apple.com/forums/thread/664295?login=true#reply-to-this-question

NOTE - LOCAL

The solution is for local package. You define

let bundleNameIOS = "LocalPackages_TargetName"
let bundleNameMacOs = "PackageName_TargetName"

REMOTE (Packages from GitHub e.g.)

If you have your package on GitHub and fetch it remotely, you can't define local packages and you have to slightly change it to:

let bundleNameIOS = "TargetName_TargetName"
let bundleNameMacOs = "TargetName_TargetName"

Example

Here is a full implementation example, you have to put it inside your package with the resources:

// Inside your package with the resources:
// Extend Bundle to access it in other packages
extension Bundle {

    // public static let assets = Bundle.module
    // Updated with workaround
    public static let assets = Bundle.myModule
    
}

Define your bundle

private class CurrentBundleFinder {}
extension Foundation.Bundle {
    static var myModule: Bundle = {
        /* The name of your package. You may have same PackageName and TargetName*/
        let bundleNameIOS = "TargetName_TargetName"
        let bundleNameMacOs = "TargetName_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,
            // -> Optional UI Tests
            /* 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(),
        ]
        
        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")
    }()
}

Accessing package A resource in package B

You can now access the resource from package A inside package B like:

let example = UIImage(named: "Exampe", in: Bundle.assets, with: nil)!
Related