How to include assets / resources in a Swift Package Manager library?
Asked Answered
G

7

51

I would like to ship my library using Apple's Swift Package Manager. However my lib includes a .bundle file with several strings translated in different languages. Using cocoapods, I can include it using spec.resource. But in SwiftPM, I cannot do it. Any solution?

Gooch answered 2/10, 2016 at 8:3 Comment(0)
K
16

Using Swift 5.3 it's finally possible to add localized resources πŸŽ‰

The Package initializer now has a defaultLocalization parameter which can be used for localization resources.

public init(
    name: String,
    defaultLocalization: LocalizationTag = nil, // New defaultLocalization parameter.
    pkgConfig: String? = nil,
    providers: [SystemPackageProvider]? = nil,
    products: [Product] = [],
    dependencies: [Dependency] = [],
    targets: [Target] = [],
    swiftLanguageVersions: [Int]? = nil,
    cLanguageStandard: CLanguageStandard? = nil,
    cxxLanguageStandard: CXXLanguageStandard? = nil
)

Let's say you have an Icon.png which you want to be localised for English and German speaking people.

The images should be included in Resources/en.lproj/Icon.png & Resources/de.lproj/Icon.png.

After you can reference them in your package like that:

let package = Package(
    name: "BestPackage",
    defaultLocalization: "en",
    targets: [
        .target(name: "BestTarget", resources: [
            .process("Resources/Icon.png"),
        ])
    ]
)

Please note LocalizationTag is a wrapper of IETF Language Tag.

Credits and input from following proposals overview, please check it for more details.

Kitchenette answered 1/5, 2020 at 17:24 Comment(1)
How would I access a resource within a Swift Package assuming the above is followed? Ex: Parent app has a Swift package "ImageHelper" and I want to access a specific image resource within from the parent app, how is this done? Bundle.Something.path? – Pail
T
44

The package manager does not yet have any definition for how resources will be bundled with targets. We are aware of the need for this, but don't yet have a concrete proposal for it. I filed https://bugs.swift.org/browse/SR-2866 to ensure we have a bug tracking this.

Tomfoolery answered 5/10, 2016 at 15:39 Comment(4)
There's now a draft proposal for this: forums.swift.org/t/draft-proposal-package-resources/2994 – Samaritan
Slight typo above, draft proposal is forums.swift.org/t/draft-proposal-package-resources/29941 – Winstonwinstonn
Looks like the proposal passed as SE-0271. Still needs to be implemented though. – Conflux
Update : It is coming for sure with Swift 5.3 released somewhere between May & September 2020. forums.swift.org/t/se-0271-package-manager-resources/30730/74 – Misappropriate
K
16

Using Swift 5.3 it's finally possible to add localized resources πŸŽ‰

The Package initializer now has a defaultLocalization parameter which can be used for localization resources.

public init(
    name: String,
    defaultLocalization: LocalizationTag = nil, // New defaultLocalization parameter.
    pkgConfig: String? = nil,
    providers: [SystemPackageProvider]? = nil,
    products: [Product] = [],
    dependencies: [Dependency] = [],
    targets: [Target] = [],
    swiftLanguageVersions: [Int]? = nil,
    cLanguageStandard: CLanguageStandard? = nil,
    cxxLanguageStandard: CXXLanguageStandard? = nil
)

Let's say you have an Icon.png which you want to be localised for English and German speaking people.

The images should be included in Resources/en.lproj/Icon.png & Resources/de.lproj/Icon.png.

After you can reference them in your package like that:

let package = Package(
    name: "BestPackage",
    defaultLocalization: "en",
    targets: [
        .target(name: "BestTarget", resources: [
            .process("Resources/Icon.png"),
        ])
    ]
)

Please note LocalizationTag is a wrapper of IETF Language Tag.

Credits and input from following proposals overview, please check it for more details.

Kitchenette answered 1/5, 2020 at 17:24 Comment(1)
How would I access a resource within a Swift Package assuming the above is followed? Ex: Parent app has a Swift package "ImageHelper" and I want to access a specific image resource within from the parent app, how is this done? Bundle.Something.path? – Pail
S
7

starting on Swift 5.3, thanks to SE-0271, you can add bundle resources on swift package manager by adding resources on your .target declaration.

example:

.target(
   name: "HelloWorldProgram",
   dependencies: [], 
   resources: [.process(Images), .process("README.md")]
)

if you want to learn more, I have written an article on medium, discussing this topic

Seeress answered 22/5, 2020 at 17:35 Comment(0)
D
5

Due to framework bundles not being supported yet, the only way to provide bundle assets with an SPM target is through a Bundle. If you implement code in your framework to search for a particular bundle in your main project (supporting asset bundles), you can load resources from said bundle.

Example:

Access the bundled resources:

extension Bundle {
    static func myResourceBundle() throws -> Bundle {
        let bundles = Bundle.allBundles
        let bundlePaths = bundles.compactMap { $0.resourceURL?.appendingPathComponent("MyAssetBundle", isDirectory: false).appendingPathExtension("bundle") }
        
        guard let bundle = bundlePaths.compactMap({ Bundle(url: $0) }).first else {
            throw NSError(domain: "com.myframework", code: 404, userInfo: [NSLocalizedDescriptionKey: "Missing resource bundle"])
        }
        return bundle
    }
}

Utilize the Bundled resources:

let bundle = try! Bundle.myResourceBundle()
return UIColor(named: "myColor", in: bundle, compatibleWith: nil)!

You can apply the same logic for all resource files, including but not limited to storyboards, xibs, images, colors, data blobs, and files of various extensions (json, txt, etc).

Note: Sometimes this makes sense, sometimes it doesn't. Determine use to own project's discretion. It would take very specific scenarios to justify separating Storyboards/Xibs into bundled assets.

Disoperation answered 27/9, 2019 at 13:5 Comment(0)
N
3

The solution I use for this is to build the data I need into a Swift object. To this end I have a shell script that will read an input file, base64 encode it, then write a Swift file that presents it as an InputStream. Then, when I want to add a data item to my Swift package, I run the script to read the file and write the output file. Of course the output file needs to be checked in so that the resource is available to those who use the project even if they do not have the script. (Typically I place my input files in a Resources directory and write the output to the Sources directory, but the script itself does not depend on that.)

I consider this a less than ideal solution, and am looking forward to when the package manager has this ability built in. But in the meantime, it is a workable solution.

The following example shows how it is used:

First, here is the script itself:

#!/usr/bin/env bash

# Read an input file, base64 encode it, then write an output swift file that will
# present it as an input stream.
#
# Usage: generate_resource_file.sh <inputfile> <outputfile> <streamName>
#
# The <streamName> is the name presented for the resulting InputStream. So, for example,
#   generate_resource_file.sh Resources/logo.png Sources/Logo.swift logoInputStream
# will generate a file Sources/Logo.swift that will contain a computed variable
# that will look like the following:
#   var logoInputStream: InputStream { ...blah...
#

set -e

if [ $# -ne 3 ]; then
    echo "Usage: generate_resource_file.sh <inputfile> <outputfile> <streamName>"
    exit -1
fi

inFile=$1
outFile=$2
streamName=$3

echo "Generating $outFile from $inFile"
echo "Stream name will be $streamName"

if [ ! -f "$inFile" ]; then
    echo "Could not read $inFile"
    exit -1
fi

echo "// This file is automatically generated by generate_resource_file.sh. DO NOT EDIT!" > "$outFile"
echo "" >> "$outFile"
echo "import Foundation" >> "$outFile"
echo "" >> "$outFile"
echo "fileprivate let encodedString = \"\"\"" >> "$outFile"
base64 -i "$inFile" >> "$outFile"
echo "\"\"\"" >> "$outFile"
echo "" >> "$outFile"
echo "var $streamName: InputStream {" >> "$outFile"
echo "    get {" >> "$outFile"
echo "        let decodedData = Data(base64Encoded: encodedString)!" >> "$outFile"
echo "        return InputStream(data: decodedData)" >> "$outFile"
echo "    }" >> "$outFile"
echo "}" >> "$outFile"

echo "Rebuilt $outFile"

Then, given an input file t.dat shown here:

Hello World!

Running the command generate_resource_file.sh t.dat HelloWorld.swift helloWorldInputStream generates the following HelloWorld.swift file:

// This file is automatically generated by generate_resource_file.sh. DO NOT EDIT!

import Foundation

fileprivate let encodedString = """
SGVsbG8gV29ybGQhCgo=
"""

var helloWorldInputStream: InputStream {
    get {
        let decodedData = Data(base64Encoded: encodedString)!
        return InputStream(data: decodedData)
    }
}
Nishanishi answered 12/3, 2020 at 17:50 Comment(1)
Now that Swift Package Manager supports this in a nicer framework, you should no longer use my solution (unless of course you have to support a legacy version for some reason). Instead you should use the solution posted by @bilalreffas. – Nishanishi
R
0

If you declare a Swift tools version of 5.3 or later in your package manifest, you can bundle resources with your source code as Swift packages. For example, Swift packages can contain asset catalogs, storyboards, and so on.

To add resources to a Swift package, do any of the following:

  1. Drag them into the Project navigator in Xcode.
  2. From the File menu in Xcode, choose Add Files to [packageName].
  3. Use Finder or the Terminal app.

When you add a resource to your Swift package, Xcode detects common resource types for Apple platforms and treats them as a resource automatically. For example, you don’t need to make changes to your package manifest for the following resources:

  • Interface Builder files; for example, XIB files and storyboards
  • Core Data files; for example, xcdatamodeld files
  • Asset catalogs
  • .lproj folders you use to provide localized resources

Access resource https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package#Access-a-resource-in-code

For more: https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package

Rhizocarpous answered 2/7 at 17:33 Comment(0)
W
-1

Important note:

Resources doesn't seem to be included in the Xcode project generated by

swift package generate-xcodeproj

But they are when you open the Package folder on Xcode (xed .) and then double click on the package to resolve dependencies.

I'm including a nice tutorial as well: https://medium.com/better-programming/how-to-add-resources-in-swift-package-manager-c437d44ec593

Wilfredwilfreda answered 20/11, 2020 at 2:12 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.