swiftbundlexcasset

Find what bundle a function was called from automatically


A project I'm working on was recently split into multiple smaller projects for reasons outside of my control.

We have some helper methods in one project to create shorthand and type safe image requests... The reason for this is to have flexibility around theming. Maybe one themes detailDisclosure isn't the same as anothers.

The syntax looks like this

public extension UIImageView {
    convenience init(_ key: UIImage.Key) {
        self.init(image: UIImage(named: theme.imageName(for: key)))
    }
}

let imageView = UIImageView(.detailDisclosure)

and it's sister function

let image = UIImage(.detailDisclosure)

Pretty simple stuff when all of the images and themes live in the same place. However now we have different projects, which have different assets inside different asset folders.

So what I had to add to make this work was this...

convenience init(_ key: UIImage.Key, in locality: AnyClass? = nil) {
    self.init(image: UIImageView.localImage(named: key.rawValue, in: locality))
}

// Currently assumes this method and default assets are in the main bundle by default 
fileprivate static func localImage(named name: String, in locality: AnyClass?) -> UIImage? {
    let bundle = (locality != nil) ? Bundle(for: locality!) : Bundle.main
    return UIImage(named: name, in: bundle, compatibleWith: nil)
}

let image = UIImage(.detailDisclosure, in: ThisProjectTheme.self)

ThisProjectTheme can actually be any class inside this bundle, and technically you can go to another bundle and share its resources in this way too.

From a consumer perspective though, this extra effort is something I'd be looking to avoid, and it's also pretty dangerous for newcomers in my opinion.

What would be better, would be unless the consumer of this API specifies a different locality, we find their locality for them automatically; instead of the current solution to go to the main bundle.

In the future most of these requests will come from projects that have their own assets.

I've seen for example file: String = #file

convenience init(_ key: UIImage.Key, file: String = #file, in locality: AnyClass? = nil)

Means we can hack at it obviously, but I'm wondering if there's an elegant solution to get a sender or for that matter bundle without the consumer implicitly sending it to the function?

Thanks for your time


Solution

  • "what bundle a function was called from automatically"

    As appealing as this might sound, you reallllly don't want that. The moment you find that copy/pasting some code from one project to another makes it behave differently, is the moment you'll lose your marbles.

    Instead, I think this entire approach needs to be reworked. For one, UIImage seems to be the wrong point of abstraction for this. Instead, I would use something like this:

    import UIKit
    
    class ImageProvider {
        let bundle: Bundle
    
        init(bundle: Bundle) {
            self.bundle = bundle
        }
    
        init(forMainClass mainClass: AnyClass) {
            self.init(bundle: Bundle(for: mainClass)!)
        }
    
        func image(
            named: String,
            with configuration: UIImage.Configuration? = nil
        ) -> UIImage {
            return UIImage(named: name, in: self.bundle, with: configuration)?
        }
    }
    

    Each app will create its own ImageProvider, which searches for assets within its own bundle.

    This has several key advantages:

    1. The interface can easily be extracted into a protocol, and a mock implementation can be created for use in tests.
    2. You have a single point-of-entry into the image system, which allows you to handle caching, theming, sizing, etc.
    3. You could extend this to handle look-up into multiple bundles ("first search my app bundle, if not, try this framework's bundle")
    4. You could extend this to use an enum to identify images, rather than raw strings.