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. Code generation describes 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.