49

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?

Dan Carter
  • 587
  • 5
  • 8
danielemm
  • 1,636
  • 1
  • 14
  • 24

6 Answers6

43

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.

Daniel Dunbar
  • 3,465
  • 1
  • 21
  • 13
  • There's now a draft proposal for this: https://forums.swift.org/t/draft-proposal-package-resources/2994 – Klaas Nov 06 '19 at 21:00
  • 1
    Slight typo above, draft proposal is https://forums.swift.org/t/draft-proposal-package-resources/29941 – lewis Nov 07 '19 at 09:47
  • 1
    Looks like the proposal [passed](https://forums.swift.org/t/accepted-with-modifications-se-0271-package-manager-resources/31021) as [SE-0271](https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md). Still needs to be implemented though. – John M. Dec 02 '19 at 22:56
  • 4
    Update : It is coming for sure with Swift 5.3 released somewhere between May & September 2020. https://forums.swift.org/t/se-0271-package-manager-resources/30730/74 – Bioche Apr 05 '20 at 10:12
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.

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
BilalReffas
  • 8,132
  • 4
  • 50
  • 71
  • 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? – Jerland2 Jun 24 '20 at 02:14
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.

Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
TheCodingArt
  • 3,436
  • 4
  • 30
  • 53
5

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

Wendy Liga
  • 634
  • 5
  • 12
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)
    }
}
Steven W. Klassen
  • 1,401
  • 12
  • 26
  • 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. – Steven W. Klassen Dec 03 '20 at 00:37
-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

andreacipriani
  • 2,519
  • 26
  • 23