Add resources to Swift packages

Xcode 11 added support for the Swift Package Manager but it limited you to source code. In Xcode 12 you can add resources including things like asset catalogs, storyboards, Core Data models and more.

Adding Resources To Packages

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:

Xcode navigator showing example of a Resources 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:

  1. 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.

  2. 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):

Package directory showing 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:

Package directory showing the HTML folder is NOT preserved

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: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 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:

Adding localized files to the package

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")
      ]
    )
  ]
)

Read More