Search code examples
swiftswift-package-managerswift-package

Cannot access Swift Package xcassets


I'm trying to use a color set from a xcassets folder that is inside a Swift Package (src). It doesn't seem to be working. I tested this out by writing a simple view that attempts to make use of the color:

Text("Hello")
  .foregroundColor(Color("brandPrimary")

I got an empty view from this code; the text can't find the color.

I've done significant research around the web - reviewing WWDC videos such as Swift packages: Resources and localization, and they seem to suggest that xcassets folder are automatically included as resources. It doesn't work for me.

I tried to add process("Resources/Colors.xcassets") inside my package manifest, but that didn't help either.


Solution

  • From the Color documentation:

    init(_ name: String, bundle: Bundle? = nil)

    bundle

    The bundle in which to search for the color resource. If you don’t indicate a bundle, the initializer looks in your app’s main bundle by default.

    Your GoodPackage library's assets are not the app's main bundle, so you need to tell the Color initializer which bundle to search.

    The Swift package manager's build process automatically creates a Bundle for each target/module that contains assets. Within the module, you can access that generated Bundle using the expression Bundle.module. SwiftPM actually writes Swift code to a file named resource_bundle_accessor.swift in your DerivedData to make this work. The generated source code looks like this:

    import class Foundation.Bundle
    import class Foundation.ProcessInfo
    import struct Foundation.URL
    
    private class BundleFinder {}
    
    extension Foundation.Bundle {
        /// Returns the resource bundle associated with the current Swift module.
        static let module: Bundle = {
            let bundleName = "YourPackageName_YourTargetName"
    
            let overrides: [URL]
            #if DEBUG
            if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] {
                overrides = [URL(fileURLWithPath: override)]
            } else {
                overrides = []
            }
            #else
            overrides = []
            #endif
    
            let candidates = overrides + [
                // 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: BundleFinder.self).resourceURL,
    
                // For command-line tools.
                Bundle.main.bundleURL,
            ]
    
            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 YourPackageName_YourTargetName")
        }()
    }
    

    As you can see, that static let module property has (default) internal access, so you can only use it from source code within that module. By default, there is no way to access the module's Bundle from outside the module.

    One solution is to add a public accessor for the module Bundle in the GoodPackage module. For example, add this to a source file in the GoodPackage module:

    import Foundation
    
    extension Bundle {
        public var GoodPackage: Bundle { Bundle.module }
    }
    

    Then, in your app:

    import GoodPackage
    
    ...
    
        Text("Hello")
            .foregroundColor(Color("brandPrimary", bundle: .GoodPackage))