Swift package
Public, bundle-aware resources with flexible loaders and typed string-catalog support
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
)
}
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.
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.
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"
)
]
)
]
)
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.
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
)
}
}
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.
| Capability | What it preserves |
|---|---|
| Public generated descriptors | Resource access across package and target boundaries |
PackageResourcesCore | Names, bundles, identity, and a foundation for custom APIs |
PackageResources | Standard SwiftUI, UIKit, AppKit, Core Graphics, font, scene, and localization loaders |
| Typed localization factories | Catalog key, table, bundle, argument types, order, and locale |
package-resources-cli | Repeatable 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.