- Proposal: SE-NNNN
- Authors: Cihat Gündüz
- Review Manager: TBD
- Status: Work in Progress
This proposal adds support for resources in Swift packages by enhancing the package manifest to provide a modern and safe way of accessing any type of resources from within other targets in the same package or by clients directly. The design is easy enough to enable fast adoption for common types, flexible enough to make typed access possible for custom resource types and extensible enough for subsequent improvements like asset variants (e.g. Dark mode, device size, locale) in later proposals.
As of today packages can't provide features which require the inclusion of resources like binary data (e.g. fonts, images, audio) or string data (e.g. HTML, JSON, SVG). This prevents a whole set of packages to be shared by teams across projects or provided to a wider community, including (but not limited to) many UI or media related packages.
Such packages might either want to provide their resources to clients they are integrated into ("public resources"), or use them as an implementation detail within their publicly accessible functionality ("private resources"). Combining both modes (some public and some private resources) might also be a valid use case.
All of this was repeatedly asked for by the community (e.g. in this thread on the forums), especially to sometime replace existing package managers like Carthage and CocoaPods with a more integrated solution by Apple, which has become true as of the WWDC 2019, where SwiftPM was integrated directly into Xcode. The authors of this proposal believe that missing support for resources is one of the biggest obstacles potentially holding off the community from adopting SwiftPM over the existing solutions in their Apple platform targeting projects.
The proposed solution consists of three parts:
- Changes to the Package Manifest format
- Generated Constants
- Wrapper Protocols for typed Resources
First, we want to provide a new type of target to the manifest file specifically for resources, named resourceTarget
consisting of a name
and an optional path
. Other targets (like target
, testTarget
) should then be able to specify them as dependencies within their dependencies
parameter. Also product types (like library
, executable
) should be able to specify them as included targets within their targets
parameter. As a result, a package manifest might look like this:
import PackageDescription
let package = Package(
name: "MyLibrary",
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
targets: [
.target(
name: "MyLibrary",
dependencies: ["MyLibraryResources"]
),
.resourceTarget(name: "MyLibraryResources")
]
)
Note that any resourceTarget
is placed under Resources
instead of Sources
by default. The "MyLibraryResources" folder can either directly include resources in which case they would be automatically provided under an enum named like the library and typed as Data
. The Enum might look something like this:
enum MyLibraryResources {
static let myImage: Data
static let myFont: Data
}
Secondly, the resource target can also contain a folder named ResourceWrappers
where it places Swift files implementing the new ResourceWrapper
protocol. Additionally, there can be subfolders named after the wrappers which contain data the wrappers provide direct access to. The ResourceWrapper
protocol looks as follows:
protocol ResourceWrapper {
associatedtype T
static func make(data: Data, fileName: String, fileExtension: String?) -> T?
}
It's API is very simple and mainly is there for initializing the type from a Data
object. Optional parameters like context
could be added in a later proposal for loading different variants of resources depending on some context information but that feature is out of scope of this proposal.
As a result of this, both targets including the resource target and clients could load typed resource safely by calling something like MyLibraryResources.myImage
which could be a UIImage
/NSImage
with a wrapper which differs between the platforms.
Also there could be grouping of resources, both under the wrapper type names (e.g. UIImage/myImage.png
) and on top level, specifying the wrapper types later down the road (e.g. Onboarding/UIImage/myImage.png
). In these cases the generated enum would create subtypes to reflect the structure like this:
enum MyLibraryResources {
enum Onboarding {
static let myImage: UIImage
static let myFont: UIFont
}
}
When building the same framework for the macOS platform, the types would be changed to NSImage
and NSFont
accordingly by implementing the wrappers something like this:
#if os(iOS)
typealias ImageWrapper = UIImageWrapper
import UIKit
struct UIImageWrapper: ResourceWrapper {
static func make(data: Data, fileName: String, fileExtension: String?) -> UIImage? {
return UIImage(data: data)
}
}
#elseif os(macOS)
typealias ImageWrapper = NSImageWrapper
import AppKit
struct NSImageWrapper: ResourceWrapper {
static func make(data: Data, fileName: String, fileExtension: String?) -> NSImage? {
return NSImage(data: data)
}
}
#endif
Note that while the type is ending with Wrapper
, SwiftPM will remove that suffix and search for Image
only.
TODO: ...
Does this change have any impact on security, safety, or privacy?
This is a purely additive change and should therefore have no impact on existing packages.
Alternatively, instead of introducing a new resource type to the package manifest format, we could simply stop ignoring unknown file types in normal targets and make them accessible via some generic API like PackageName.loadResource(named: "X")
which could provide a Data
object if existent or nil
. This approach does not comply with Swifts goals to be a modern and safe language though. See this proposal draft from Anders Bertelrud for more information.