Swift package

swift-package-resources

Public, bundle-aware resources with flexible loaders and typed string-catalog support

Resources that cross module boundaries

Xcode can generate convenient symbols for asset catalogs, but those declarations are internal to the module that owns the resources. That works inside an application target; it becomes limiting when a design-system or feature package needs to expose its assets to other modules.

swift-package-resources generates lightweight descriptors with the access level you choose. A public declaration carries its exact asset name and package bundle across the module boundary:

import PackageResourcesCore

extension PackageResources.Color {
  public static let accent = Self(
    name: "Accent",
    bundle: .module
  )
}

extension PackageResources.Image {
  public static let appLogo = Self(
    name: "App Logo",
    bundle: .module
  )
}
DesignSystem/Resources.generated.swift

A consuming module uses those same descriptors through SwiftUI, UIKit, or AppKit without trying to access another target’s Bundle.module:

import DesignSystem
import PackageResources
import SwiftUI

let color = Color.resource(.accent)
let image = Image.resource(.appLogo)
let nativeImage = UIImage.resource(.appLogo)

Catalog and folder grouping can add namespaces when multiple targets intentionally expose similarly named resources.

Start with the default API, or build your own

The package separates resource identity from loading behavior:

  • PackageResourcesCore provides small Hashable and Sendable models for generated code and custom APIs.
  • PackageResources re-exports the core and adds the default Apple-platform loaders for colors, images, fonts, storyboards, SceneKit scenes, and localized strings.

The default API is ready to use, but it is not required. A project can import only the core and put its own error policy, caching, image configuration, or dependency boundary on top:

import PackageResourcesCore
import UIKit

extension UIImage {
  static func designSystemResource(
    _ resource: PackageResources.Image,
    configuration: UIImage.Configuration? = nil
  ) -> UIImage? {
    UIImage(
      named: resource.name,
      in: resource.bundle,
      with: configuration
    )
  }
}

Generated declarations stay the same whichever API consumes them. This keeps code generation focused on stable data instead of baking one application’s loading policy into every generated file.

Make code generation part of the package

The recommended setup attaches package-resources-cli as a SwiftPM build-tool plugin and links the API product needed by the target:

// swift-tools-version: 6.0

import PackageDescription

let package = Package(
  name: "DesignSystem",
  dependencies: [
    .package(
      url: "https://github.com/capturecontext/swift-package-resources.git",
      .upToNextMajor(from: "5.0.0")
    ),
    .package(
      url: "https://github.com/capturecontext/package-resources-cli.git",
      .upToNextMajor(from: "4.0.0")
    )
  ],
  targets: [
    .target(
      name: "DesignSystem",
      dependencies: [
        .product(
          name: "PackageResources",
          package: "swift-package-resources"
        )
      ],
      resources: [.process("Resources")],
      plugins: [
        .plugin(
          name: "package-resources-plugin",
          package: "package-resources-cli"
        )
      ]
    )
  ]
)
Package.swift

Initialize the manifest once, check it into source control, and let the plugin regenerate deterministic declarations during builds:

swift package resources config init --format yaml
swift package resources generate

The companion package-resources-cli page focuses on this integration layer: output locations, declaration visibility, catalog and folder grouping, identifier casing, resource opt-outs, and package- versus target-specific configuration.

Localization as a first-class resource

String catalogs are not reduced to untyped keys. Code generation preserves the key, table, bundle, argument order, and each placeholder’s representation. A localized entry with interpolation becomes a typed factory:

extension PackageResources.LocalizedString {
  public static func inboxSummary(
    name: String,
    unreadCount: Int
  ) -> Self {
    .init(
      key: "inbox.summary",
      arguments: [
        .object(name),
        .int(unreadCount)
      ],
      table: "Localizable",
      bundle: .module
    )
  }
}
Localizable.generated.swift

The same generated value works with Foundation localization, modern LocalizedStringResource, LocalizedStringKey, and SwiftUI Text:

let summary = PackageResources.LocalizedString.inboxSummary(
  name: profile.name,
  unreadCount: inbox.unreadCount
)

let string = String.localized(summary, locale: .current)
let resource = LocalizedStringResource.localized(summary)
let key = LocalizedStringKey.localized(summary)
let text = Text(localized: summary)

Generated arguments distinguish integers, unsigned integers, floats, doubles, and objects, so format placeholders remain ordered and correctly represented. Callers can select a locale without losing the catalog’s table or package bundle, while SwiftUI receives a localization-aware value rather than an already-formatted string.

CapabilityWhat it preserves
Public generated descriptorsResource access across package and target boundaries
PackageResourcesCoreNames, bundles, identity, and a foundation for custom APIs
PackageResourcesStandard SwiftUI, UIKit, AppKit, Core Graphics, font, scene, and localization loaders
Typed localization factoriesCatalog key, table, bundle, argument types, order, and locale
package-resources-cliRepeatable integration, naming, grouping, visibility, and output policy

Native loaders retain optional return types where a resource can be missing. Generated declarations should keep exact catalog names and .module bundles instead of silently falling back to the main application bundle.