Swift PM, Bundles and Resources

I was browsing GitHub trending and saw https://github.com/JamitLabs/Accio. 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:

5 Likes

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

4 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.)

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.

1 Like