Skip to content

Instantly share code, notes, and snippets.

@Jeehut
Last active June 25, 2019 12:41
Show Gist options
  • Save Jeehut/fd7fd81d2bd1d2b5468ab62f602b72dd to your computer and use it in GitHub Desktop.
Save Jeehut/fd7fd81d2bd1d2b5468ab62f602b72dd to your computer and use it in GitHub Desktop.

Resource Targets

Introduction

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.

Motivation

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.

Proposed solution

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.

Detailed design

TODO: ...

Security

Does this change have any impact on security, safety, or privacy?

Impact on exisiting packages

This is a purely additive change and should therefore have no impact on existing packages.

Alternatives considered

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment