Swift PM, Bundles and Resources

I was browsing GitHub trending and saw GitHub - JamitLabs/Accio: A dependency manager driven by SwiftPM that works for iOS/tvOS/watchOS/macOS projects.. Looks interesting!

1 Like

Hello everyone,

as @rudedogg already mentioned Accio, I'd like to point out that I just wrote an article about it which explains it's motivation, rationale and design in more detail. Basic usage instructions are given, too.

Please check it out:

I hope you find it useful!

2 Likes

After introducing Package support in Xcode, this feature is so much needed. What is a current plan with supporting bundle targets and resources in SwiftPM? Personally, I would be okay with any format, even if it’s not super-flexible or future-proof.

There's already a proposal draft by @abertelrud which was also mentioned in this WWDC session. They specifically asked for community feedback on the proposal and implied that Apple would do any work needed to update the Xcode integration once SwiftPM implemented support for resources.

So, it is now really time to review this properly:

https://github.com/abertelrud/swift-evolution/blob/package-manager-resources/proposals/NNNN-package-manager-resources.md

7 Likes

SwiftPM is currently wrapping the Swift 5.1 release and there will likely be motion on the resources proposal after July.

7 Likes

I read the linked proposal draft, and not knowing where else to send my feedback, I will do it here.

Historically, macOS and iOS resources have been accessed primarily by name, using untyped Bundle APIs such as path(forResource:ofType:) that have assumed that each resource is stored as a separate file. [...] In the long run, we would like to do better. Because SwiftPM knows the names and types of resource files, it should be able provide type-safe access to those resources by, for example, generating the right declarations that the package code could then reference. [...] In the short term, however, we want to keep things simple and allow existing code to work without major modifications.

I am of the opinion that individual synthesized accessors should not be deferred. The typing isn’t the issue—we can defer that by making everything unspecific Data instances for now. I would rather never provide access to dynamic string identifiers or paths.

If an API like path(forResource: String, ofType: String) is provided, then the package manager and compiler can never have enough information to know which resources are actually being used, and that will prevent future optimization opportunities. The package could do this:

public func cardImage(suit: Suit, rank: Rank) -> Image {
    let name = suit.identifier + rank.identifier
    let file = Bundle.moduleResources.path(forResource: name)
    return Image(from: file)
}

With this ability, the package manager must naively bundle all available resources—not just the 52 card images, but also the cribbage board and peg images.

A client package might be making a solitaire game instead, but want to reuse the 52 card logic, so it links and calls cardImage(suit:rank:). But now they come bundled with board and peg images it does not want and will never use. The dead code contains dead resources that the optimizer has no means to identify.

I think it would be better never to provide dynamic identifiers like that, so that we keep the door open for better automatic optimization in the future.

An API where each resource is a direct property is much more future‐proof:

public func cardImage(suit: Suit, rank: Rank) -> Image {
    let data: Data
    switch suit {
    case .hearts:
        switch rank {
            case .ace:
                  data = Bundle.moduleResources.aceHearts
            case .two:
                  data = Bundle.moduleResources.twoHearts
            // ...
        }
        // ...
    }
    return Image(from: data)
}

Since every access is forced through the same property, when that property is optimized away, the optimizer knows it can remove the resource it represents too. The client only calls cardImage(suit:rank:), and never anything that uses moduleResources.cribbageBoard? Then the client needs AceHearts.png (and co.), but not Cribbage Board.png.

(I believe the vast majority of resource access likely directly loads individual and specific resources, and will not suffer the boilerplate bloat demonstrated by the above function.)

1 Like

Hi there!

Exactly like @SDGGiesbrecht, I read the mentioned proposal and wanted to leave some comments.

I wanted to highlight how we currently manage resources in our iOS project. We use Cocoapods to manage our dependencies and had to do some preparations so that we are able to statically link our dependencies which include local development pods (i.e. our feature modules). :smile:

As described in the initial post, copying all resources into the main bundle can lead to name collisions and run time crashes. For our local development pods, we therefore moved from using .resources in our podspecs to using resource_bundle. Cocoapods then creates a new target that creates a resource bundle that is then linked into the module's code bundle.

s.resource_bundles = { 'ModuleResources' => ['Module/**/*.xib', 'Module/**/*.xcassets', 'Module/**/*.json', 'Module/**/*.png'] }

Similar to the approach described in the proposal, we use an internal static property in an extension to handle resource bundle resolution per module.

static var resourceBundleName: String = "ModuleResources"

 static var resourceBundle: Bundle {
        let codeBundle = Bundle(for: Module.self)
        guard let subBundleUrl = codeBundle.url(forResource: Module.resourceBundleName, withExtension: "bundle"),
            let resourceBundle = Bundle(url: subBundleUrl) else {
                fatalError("misconfigured module")
        }
        return resourceBundle
    }

Loading images can then be achieved by using:

UIImage(named: name, in: Bundle.resourceBundle, compatibleWith: nil)

As described in the proposal, this approach has the downside of using Bundle(for: ...) and assumes that the resource bundles are inside the same bundle as the code. Additionally, it is not platform-agnostic.

I think @SDGGiesbrecht 's idea to expose resources as Data properties directly on the moduleResources bundle is great and would also eradicate bugs in which the resource name is not equal to the name used in code. Additionally, we could use this to strip out unused assets as part of the build process. It also reminds me of the way resources are accessed in Android.

The only question that then comes to my mind is how we would actually handle file name collisions inside one resource bundle. Image having a mock.json in two separate folders, both are valid files in their respective folders, but the lookup via path(forResource: name) would fail. Also, imagine having two files in different formats but using the same filename (i.e. mock.json and mock.png). Would we then include the file extension in the property name to resolve the name collision?

1 Like

Good observations!

I imagined the files would be automatically namespaced in the product layout since the generate access code is internal, there are no filename collisions between modules. Namespacing could be either with subdirectories or by automatically prefixing the file names themselves. Note that I understand implementation details like this have room for variation by platform or product type. All that needs to be stable is (a) where/how you put/declare the resources in the package repository and (b) the Swift interface generated for their retrieval.

Resources
↳ MyPackage
  ↳ MyModule
    ↳ en
      ↳ mock.json
    ↳ de
      ↳ mock.json
↳ DependencyPackage
  ↳ DependencyModule
    ↳ mock.json

This affects more than just file extensions. Swift identifiers have a character set significantly restricted compared to what filenames have at their disposal, so it is necessarily either a lossy conversion or the result is super ugly. This is the algorithm I currently use to Swift‐ify filenames, but it would not have to be that elaborate. An ultra‐simple solution would be to simply disallow filenames that are not already valid Swift identifiers so as not to have to clean them up at all. It think it is reasonable to simply throw a compiler error if a single module has multiple resources named in a way that ends up clashing. The module author can simply rename the files.

2 Likes

I agree that keeping the access code internal would solve these problems and that was my assumption. I actually meant resource name collisions in the same module. Namespacing would be an option which we could look into and maybe also define as part of the Package.swift file. Similar to Cocoapods that allows to define multiple resource bundles instead of just one, we could use this to namespace the resources.

let package = Package(
    name: "MyInfoPanel",
    targets: [
        .target(
            name: "MyInfoPanel",
            dependencies: ["IconWidget"],
            resources: [
                .bundle(name: "en", content: [
                    "en/*.json": .resource
                ]),
                .bundle(name: "en", content: [
                    "de/*.json": .resource
                ])
            ]
        ),
        .target(
            name: "IconWidget",
            exclude: ["Border.png"]
        ),
        .testTarget(
            name: "MyInfoPanelTests",
            dependencies: ["MyInfoPanel", "MyTextFixtures"]
        ),
        .testTarget(
            name: "MyTestFixtures",
        ),
    ]
)

This would resolve name collisions and leave the flexibility to the package owner to decide how they want to structure their package resources.

We could extend the idea above to include resolve dependencies into subnamespaces (i.e. image, resource, ...). In the roles, we could define a set of possible roles that a resource can take. Resources could then be accessed via this subnamespace. Potentially, we could also return the resource to the representing type (i.e. UIImage for iOS, etc.) based on the context it is used in.

// in Package.swift
.target(
    name: "MyInfoPanel",
    dependencies: ["IconWidget"],
    resources: [
        .bundle(name: "en", content: [
            "en/*.json": .resource
        ]),
        .bundle(name: "en", content: [
            "de/*.json": .resource
        ]),
        .include(content: [
            "*.png": .image
        ])
    ]
),
...
// in the package
let image = Bundle.moduleResources.images.aceHearts // -> UIImage, ...
let image = Bundle.moduleResources.images.aceHearts.raw // -> Data

Sure, I can see how those customization could come in useful at times. I do suspect that most projects wouldn’t be complicated enough to need the extra specificity. I would suggest treating it like a target’s path or sources—customizable, but for the most part just left to an implicit default.

(In case I was unclear, the en and de directories represented localizations of the same files. I would expect the same call to resources.mock to return the contents of one or the other depending on the localization active at runtime. Localization does not need to be actually supported in the first iteration, but it does need to be a consideration so that we don’t box ourselves out by our name‐spacing design. The only kind of name clash I was demonstrating there was across separate modules. I was leaving the problem of same‐module clashes for the following paragraph as a separate issue.)

Since the feedback process on the 2018 "Package Resources" proposal draft has already started here, let me mention two requirements I'd expect from resources support in SwiftPM which I both don't see covered by the draft:

  • allow type-safe access with resource availability checks by the compiler, preventing dynamic strings (removing the need for tools like SwiftGen entirely)
  • allow folder-based types for different contexts, e.g. to make initializing different UIImage resources from a .xcassets folder or a String from a directory containing .lproj folders possible

I've actually just pitched an alternative (incomplete) proposal draft which I plan to continuously work on:

I'd love to hear your feedback on that, too!

8 Likes

Any update on this?

I think the biggest underlying issue is that modern operating systems can have quite complex resource abstractions, and there is some reluctance to model that in SwiftPM (for good reason!)

The old idea of grabbing a file by a path is doable, of course, but is much, much less than ideal in today’s world. It basically is only good for raw data, like JSON files or canned databases. Not for UI stuff. Even then, they couldn’t take advantage of things like on-demand resources. It would be very, very basic. So I can see why it’s not a huge priority.

Darwin platforms support all kinds of UI-related resource variants - from device type, screen scale and dark mode to things like which version of Metal you’re running on. And then there are localised and the aforementioned on-demand resources. And what about Linux? And Windows?

What if we start with simple resource packaging like CocoaPods provides and worry about integrating into platform and other APIs later? The lack of this feature is major drawback of SPM at the moment. It seems like providing the simplest version first would be better than waiting to nail down a single integrated abstraction for every platform.

14 Likes

Everything is still contained in a file/folder, referenceable by a filename, even if it is in an asset catalog. Unless you're thinking the package manifest has to directly address all those items you linked to in your post that are contained in the asset catalog Contents.json file.

1 Like

I read the package-manager-resources proposal too.

I think scope of such proposal is too broad to begin. In my opinion SPM should support resources in a very simple way to start. Then a follow-on proposal could refine details later.

Here is my feedback:

Accessing resources via Bundle apis and strings is probably not optimal but it is the way it would work today and also work in other platforms. A follow-on proposal could improve this with type-safe accessors, etc.

About Location of resources in a package

... This could take the form of a special Resources subdirectory under each target directory, but it seems more natural to allow the resource files to be located alongside the source files with which they are most closely associated.
This would allow grouping of package resources in the same way as sources are grouped, by just creating a directory structure under the individual target subdirectories.
This approach is, for example, common in Xcode projects today.

The main idea of the proposal seems to support resources everywhere (not just in a specific Resources folder). I think is it OK. I just would like to know what would happen with Border.png in below example?

Additionally from allowing resources everywhere, I think still having a special folder Resources inside Source folder could be useful. Everything inside Resources folder would be copied as is, maintaining its folder structure intact, This would be helpful for localization (Folders like en, es, ja, etc containing more resources, etc), for assets folder (Folders with a particular json file and images, data files, etc), etc

Package
 └  Sources
    ├ MyInfoPanel
    │  ├ InfoPanelController.swift
    │  └ Border.png // Collision with Border.png inside IconWidget ?
    ├ IconWidget
    │  ├ IconDataSource.swift
    │  └ Border.png // Collision with Border.png inside MyInfoPanel ?
    └ Resources
       ├ MyInfoPanel
       │ └ Image.png // This would be referenced via "MyInfoPanel/Image.png" ?
       ├ IconWidget
       │ └ Image.png // This would be referenced via "IconWidget/Image.png" ?
       └ Image.png // This would be referenced via "Image.png" ?

I hope we can have SPM supporting resources sometime soon
(not perfect but working if much better than not working at all)

Cheers.

2 Likes

Unless I’ve forgotten something (it has been a while since I read all this), all of the variations proposed tie each resource to a particular target.

The way outlined in proposal draft, would allow IconDataSource.swift to ask for "Border.jpg" and get the one beside it inside IconWidget (assuming the IconWidget directory represents module). InfoPanelController.swift could ask for "Border.jpg" and get the one beside it inside MyInfoPanel. SomeOtherModule would not get either if it requested "Border.jpg".

An alternative suggested in the comments above is to move the resource files to a separate Resources directory with a parallel structure to the Sources directory. That is basically what the second half of your diagram describes. In this case InfoPanelController.swift could ask for "Image.png" and get the one at Resources/MyInfoPanel/Image.png. And IconDataSource.swift’s request for "Image.png" would fetch it Resources/IconWidget/Image.png. The final entry in your chart, Resources/Image.png wouldn’t be a valid place to put a resource, because it isn’t associated with a target; it would be unrecognized and ignored by the package manager just like a LICENSE.md file at the package root.

Earlier I expressed concerns about the optimizability of querying for resources with string names. After more thought, I think I would be okay with it as a starting point, as long as its intended optimizability is clarified and locked in (even if not implemented until some later date).

I think the string‐based query should be considered by the optimizer as an access to all resources in a target. That means if the string‐based access is used somewhere, the optimizer must keep all the resource files around (unless it can prove by some StaticString shenanigans that the accesses are all only to specific files, but that would be super advanced). On the other hand if the string‐based access method turns out to be dead code itself, then the optimizer will be free to strip out all the resources (excluding those accessed through some other improved future API).

With that logic the string‐based access can be available as a simpler starting point for this feature and as a smaller shift for existing code, but it wouldn’t block any future optimization for those packages that refrain from using it that way.

A few SwiftPM contributors (@neonacho, @aciid, @rballard, and me) met to discuss the draft proposal for package resources from last year, especially in light of the recent discussion and pitch for resource targets on the Swift forums.

The discussion came to the following observations:

  • In the interest of providing incremental improvements, we believe that the initial proposal should focus on how resources are defined in packages, and provide only the basic means necessary to access them at runtime (through Bundle, initially). We all agree that type-safe access to individual resources is important in the long-term, but believe that it can be separated into a follow-on proposal that builds on the initial one. Adding type-safe access later should not introduce incompatibilities with existing packages or prevent optimizations, and we shouldn’t delay the basic support for resources while discussing how to get the type-safe access correct across the various platforms.

  • The basic choice for how resources should be specified comes down to whether to require all resources to be put into a directory with a special name (such as “Resources”) or whether to allow resources to be intermixed with source files. There are a number of tradeoffs, and the answer is less clear for resource types that are processed to generate both data and source code. Using a directory with a special name might be simpler for the most common cases, but might also require files to be moved in the file system when adding SwiftPM support for existing code bases that don’t already have their files arranged that way. On the other hand, allowing resources to be intermixed with source code is more flexible, and perhaps a more natural fit for file types that conceptually straddle the boundary between source code and resources (such as Metal shaders), but requires some way of indicating which files should be treated as resources based on their type alone. Those rules can get complicated and cause confusion.

  • There is a question of how much to teach SwiftPM about resource types that are special to various platforms and IDEs, such as .xib files and .storyboards, while keeping the basic feature generic enough to support new platforms, IDEs, and resource types in the future. How extensible is this to other file types on other platforms?

  • Since the .xcassets asset catalog format is a JSON-based documented format, there was discussion about whether it could be used for specifying resource variants (for all platforms). This would provide a standard way to specify variations, such as light vs dark mode, on all platforms that support such distinctions. This would require SwiftPM to be able to read the JSON files that are part of that format.

The next step will be to update the draft proposal with more details and alternatives, focusing on the specific goals that the feature needs to address.

15 Likes

The thing I’m concerned about is that the simple, path/Bundle-based way of accessing resources is not normally recommended for Darwin platforms (at least for images). Files in an asset catalog don’t even have a unique file path to access from (and of course, we would want resources for Darwin platforms to end up in an asset catalog when compiled). Perhaps that could be addressed later, but I’m concerned about giving people an easy way to do the wrong thing and leaving future plans to do it correctly unspecified.

I like the idea of SwiftPM reading/writing xcassets files, or perhaps having its own, similar way of specifying resource variants from which it could generate the appropriate manifests for each platform, to the best of its ability.

Perhaps we need to split the concept of resources in to simple, non-localised blobs (for which we can simply stick a file in the bundle and access via a path), and more complex things like UI resources which will be handled totally differently. Perhaps it is just too simplistic to group all these things under the umbrella of „resources“.

1 Like