Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI preview crashes when accessing assets from a nested Swift Package

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

like image 720
Mojtaba Hosseini Avatar asked Nov 06 '22 01:11

Mojtaba Hosseini


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)!
like image 79
Luca Avatar answered Nov 15 '22 12:11

Luca