skip to Main Content

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

2

Answers


  1. 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)!
    
    Login or Signup to reply.
  2. This workaround worked for me in Xcode 14.2

    private extension Bundle {
        private static let packageName = "PACKAGE_NAME"
        private static let moduleName = "MODULE_NAME"
        
        #if targetEnvironment(simulator)
        static var swiftUIPreviewsCompatibleModule: Bundle {
            final class CurrentBundleFinder {}
    
            let isPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
            
            guard isPreview else {
                return Bundle.module
            }
            
            // This is a workaround for SwiftUI previews
            // previews crash when accessing other package view using assets from Bundle.module
            
            let bundleName = "(packageName)_(moduleName).bundle"
            
            func bundle(stepsBack: Int) -> Bundle? {
                var bundleURL = Bundle(for: CurrentBundleFinder.self).bundleURL
                for _ in 1...stepsBack { bundleURL.deleteLastPathComponent() }
                bundleURL.appendPathComponent(moduleName)
                bundleURL.appendPathComponent("Products")
                bundleURL.appendPathComponent("Debug-iphonesimulator")
                bundleURL.appendPathComponent("PackageFrameworks")
                
                let directories: [String]
                do {
                    directories = try FileManager.default.contentsOfDirectory(atPath: bundleURL.path)
                } catch {
                    return nil
                }
                
                guard let matchingDir = directories.first(where: { $0.hasSuffix(".framework") }) else {
                    return nil
                }
                
                bundleURL.appendPathComponent(matchingDir)
                bundleURL.appendPathComponent(bundleName)
                
                return Bundle(url: bundleURL)
            }
            
            // Steps back 5 is a workaround for crashes
            // when another module is importing this module
            return bundle(stepsBack: 5) ?? .module
        }
        #else
        static var swiftUIPreviewsCompatibleModule: Bundle { .module }
        #endif
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search