Xcode 11 added support for the Swift Package Manager but it limited you to source code. In Xcode 12, and later, you can add resources including things like asset catalogs, storyboards, Core Data models and more.
Last updated: Aug 19, 2024
Adding Resources
The Swift Package Manager (SPM) allows you to share code between projects or even between targets within a project. For a recap see this earlier post on Creating Swift Packages in Xcode.
Starting with Xcode 12 you can also include non-source files in a package. Apple recommends you create a Resources folder under the Sources folder but it’s not required. Xcode knows how to handle common Apple resource types out-of-the box. These include (not an exhaustive list):
- Asset Catalogs (.xcassets)
- Storyboards and NIBs (.storyboard, .xib, .nib)
- Core Data models (.xcdatamodel, xcmappingmodel)
- Localization folders (.lproj)
If you include a file type that Xcode doesn’t recognize as a resource you’ll get a build error telling you to declare it in the package manifest file (Package.swift
):
found 1 file(s) which are unhandled; explicitly declare them as resources or exclude from the target
You declare resources as part of the target definition. For example, here’s my package with a target named Shared
under the Sources
folder:
The swift files and asset catalog are fine but you must declare the contents of the HTML folder and the config.json file as resources for the target in the package manifest:
targets: [
.target(
name: "Shared",
resources: [
.process("Resources/config.json"),
.copy("Resources/HTML")
]
)
]
There are two ways to declare a resource:
-
A process rule: Applies platform specific rules to the resource such as compressing png files. If there are no specific rules for a resource the file is copied to the top-level directory of the bundle. You can specify a directory to have Xcode process the contents. The directory structure is not preserved.
-
A copy rule: Copies files untouched. If you pass a directory the contents are copied preserving the sub-directory structure.
In my example I’m using a process rule for the JSON file and copying the HTML folder. The resulting package bundle looks like this (note the HTML folder is preserved):
Xcode doesn’t do anything special for JSON files so I could have used a copy rule. If we didn’t care about the directory structure we could have also used a single process rule on the Resources directory:
targets: [
.target(
name: "Shared",
resources: [
.process("Resources")
]
)
]
Since there are no special rules for any of these resource types this copies everything to the top-level directory of the package bundle:
This is also useful for test data:
.testTarget(
name: "SharedTests",
dependencies: ["Shared"],
resources: [
.process("TestData")
]
)
Minimum Tools Version
One word of caution. Swift Package Manager support for resources depends on Swift 5.3 shipping with Xcode 12. If you create a new package with Xcode 12 you’ll see this in the first line of the package manifest file:
// swift-tools-version:5.3
If you want to create a package that you can use with older versions of Xcode you’ll need to specify a version of Swift supported by that version of Xcode (and avoid using resources):
// swift-tools-version:6.0 -- Xcode 16.0
// swift-tools-version:5.10 -- Xcode 15.3
// swift-tools-version:5.9 -- Xcode 15.0
// swift-tools-version:5.8 -- Xcode 14.3
// swift-tools-version:5.7 -- Xcode 14.0
// swift-tools-version:5.6 -- Xcode 13.3
// swift-tools-version:5.5 -- Xcode 13.0
// swift-tools-version:5.4 -- Xcode 12.5
// swift-tools-version:5.3 -- Xcode 12.0
// swift-tools-version:5.2.4 -- Xcode 11.5
// swift-tools-version:5.2 -- Xcode 11.4
// swift-tools-version:5.1 -- Xcode 11.0
If you created a package with an earlier version of Xcode it will work with Xcode 12 but you cannot add resources until you change the tool version in Package.swift
to at least 5.3.
Excluding Files
If you have files in the Sources
folder that you do NOT want copied into the package bundle you can exclude them from the target:
targets: [
.target(
name: "Shared",
exclude: ["Resources/TODO.md"],
resources: [
.process("Resources/config.json"),
.copy("Resources/HTML")
]
)
]
Note that the exclude must come before the resources. You can exclude a directory.
Localizing Resources
To localize any resource in the package you must declare the default localization in the package manifest. For example, if my development language is English (“en”):
let package = Package(
name: "Shared",
defaultLocalization: "en",
Create the directories with the localized resources for the languages you are supporting. Follow the usual Xcode naming convention for .lproj
directories using the ISO 639 language code. Don’t forget to create the default en.lproj
directory.
If you are including localized storyboard or XIB files put them in the base localization directory Base.lproj
and add the strings files to the language specific .lproj
directories:
Remember that you can also localize image assets in the asset catalog.
Using Resources From A Package
The Swift Package Manager creates a static extension on Bundle
for the package module. You access the resource by specifying Bundle.module
as the bundle. Some examples:
// SwiftUI load image from asset catalog
Image("Star", bundle: .module)
// UIKit load image from asset catalog
let image = UIImage(named: "Star", in: .module, compatibleWith: nil)
// Get URL of config.json
let configURL = Bundle.module.url(forResource: "config", withExtension: "json")
For localized resources:
// Localized string
let ratingTitle = NSLocalizedString("Rating", bundle: .module, comment: "Rating title")
// SwiftUI text view
Text("Rating", bundle: .module)
// Storyboard
let ratingStoryboard = UIStoryboard(name: "Rating", bundle: .module)
Note that resources don’t default to being accessible outside the package bundle. If you want to access a resource from outside the package module create a publicly visible property. For example, assuming I have a SharedResource.swift
in the package:
// SharedResource.swift
import Foundation
public enum SharedResource {
static public let configURL = Bundle.module.url(forResource: "config", withExtension: "json")
}
I can now access the URL for the JSON resource anywhere I import the Shared
package:
let url = SharedResource.configURL
Example Package Manifest
A more complete example of a Package.swift
file:
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Shared",
defaultLocalization: "en",
platforms: [
.iOS(.v13)
],
products: [
.library(
name: "Shared",
targets: ["Shared"]),
],
dependencies: [
],
targets: [
.target(
name: "Shared",
exclude: ["Resources/TODO.md"],
resources: [
.process("Resources/config.json"),
.copy("Resources/HTML")
]
),
.testTarget(
name: "SharedTests",
dependencies: ["Shared"],
resources: [
.process("TestData")
]
)
]
)