Monday, June 29, 2020

SwiftPM and Resources

Last year I wrote about the sad state of dependency management for iOS projects. Later this year Xcode 12 will be released and it will address a couple of the big limitations of the Swift Package Manager (SwiftPM). I won't go into much detail about how they work, but you can watch a couple of great (and short!) WWDC videos about Resources and Localization and Binary Frameworks.

Last week I spent some time with the Xcode 12 beta trying the new support for resources, and even had a few great lab appointments with Apple engineers. Here are some observations. 

Resources

Xcode does a good job of bundling resources found among your sources, if it knows what to do with them. It automatically processes things like Xibs and asset catalogs. For things that it doesn't know about, you can tell it what to do in a list of resources for each target in Package.swift. See the aforementioned video for details. 

Bundle.module

If you are converting an existing framework that contains resources to SwiftPM, you'll have to make some adjustments. In a framework that is built from a traditional Xcode project, it is common to load resources such as images and Xibs using Bundle(for: AnyClass). This initializer does not work in a Swift package. Instead, Swift package code should use Bundle.module. While this is new with Xcode 12, thankfully it is backward deployable and can by used by iOS versions prior to  iOS 14. 

There is a catch though. Bundle.module is only available when building a Swift package. So, an xcodeproj framework can't use Bundle.module and has to continue to use Bundle(for: AnyClass), but a Swift package can't use Bundle(for: AnyClass) and has to use Bundle.module. This is a problem for many framework vendors that will need to support Carthage and SwiftPM concurrently for some time. It is also a problem for internal frameworks as we work toward SwiftPM, but will continue to build frameworks as Xcode projects until Xcode 12 ships. One solution is to migrate existing code to Bundle.module, and then add something like this to your project: 

#if !SWIFT_PACKAGE
extension Bundle {
    class BundleClass { }
    static let module = Bundle(for: BundleClass.self)
}
#endif

This provides an alternate implementation of Bundle.module when not building with SwiftPM. Perhaps this will be resolved before Xcode 12 ships (FB7792343 for Apple folks). 

Interface Builder

Although Xibs are now being properly copied into the target bundle, I noticed they weren't being loaded properly at runtime. Eventually I determined that I have to uncheck "Inherit Module From Target" for any custom views that are used. 
In order to make sure I caught all instances of this, I edited a lot of Xibs in a text editor. I suspect the need to uncheck "Inherit Module From Target" is a bug (FB7793693). Hopefully it will be fixed eventually. 

Testing... 

The trouble with both the Bundle(for: AnyClass) change and the Interface Builder custom class issue, is that these problems are not caught at compile time. Both result in runtime bugs or crashes. Also, these types of problems are often not covered by unit tests. If you are converting an existing framework with resources to SwiftPM, you'll need to carefully audit the project and test thoroughly. 

A Light at the End of the Tunnel

Many framework vendors already support SwiftPM. Hopefully the new support for resources and binary frameworks will enabled the rest to adopt SwiftPM soon. I look forward to fully embracing SwiftPM later this year.