🖼️ iOS Best Practices: Don't Use Dynamic Strings to Init Images
Understand why using dynamic strings (e.g. string concatenation and interpolation) to initialize UIImages is a bad idea.
As an engineer and avid programmer, you’re always identifying patterns in everything you see, so if you face something like this:
enum Direction {
case left, right, up, down
var icon: UIImage {
switch self {
case .left: return UIImage(named: "ic-left")!
case .right: return UIImage(named: "ic-right")!
case .up: return UIImage(named: "ic-up")!
case .down: return UIImage(named: "ic-down")!
}
}
Your OCD is triggered and your hands start shaking immediately. “I MUST refactor this code to simplify it! Let’s factor out the similar logic, and make everything much simpler!”
enum Direction: String {
case left, right, up, down
var icon: UIImage {
return UIImage(named: "ic-\(rawValue)")!
}
}
Ahhh, much better! I just improved this code so much!
Let’s understand below why this is not a good idea when scaling your codebase.
For the purposes of this article, “dynamic strings” refer to any string value that can’t be represented as a StaticString, i.e. text that isn’t known during compile time. This includes strings composed by string interpolation, string concatenation, and using string variables.
1. Unfortunately, UIImage initialization isn't very safe in iOS
Due to the fact that we’re force unwrapping the UIImage initialization above, if anything goes wrong when initializing that image, that code would crash. This could happen when introducing a new enum case, for instance: the developer adding the new enum case could easily miss the fact that the icon computed variable is using the enum’s raw value to initialize (and force unwrap) an image, and forget to add its associated image to the project’s xcassets resource folder, resulting in a crash.
When opting for the explicit initialization of each image (the first snippet), the developer would get a compile-time error when they add a new enum case, which would force them to think about how to handle it, and thus add the image resource to the project.
Whenever possible, always opt for compile-time safety measures.
2. Tools that identify unused resources aren’t as effective
There are amazing tools out there like LSUnusedResources that help you identify and delete unused assets in your project. However, these tools will most likely use regex expressions to search for usages of your assets in your codebase, and if you disguise your UIImages using string concatenation, interpolation, or other forms of dynamic strings, it clearly won’t find them and thus wrongfully mark them as unused assets. If you blindly trust the tool (something you shouldn’t be doing anyway) and delete those assets, you’ll get runtime crashes the next time you run your app and try to access those assets.
3. Developers looking for usage of assets won’t find them
A curious developer sees those assets named ic-down
, ic-left
, etc… and thinks:
— I wonder where they’re being used?
He then searches for "ic-down”, “ic-left”, etc… but can’t find anything other than the asset itself. No use cases.
He has two options now: prematurely conclude that they’re not being used and delete them (which would be the wrong thing to do), or try searching for substrings (randomly), such as “left” (which would earn hundreds of unrelated results), or try to narrow them down by searching for “ic-” (again, potentially hundreds of unrelated results), or “-left” (potentially no results, in our example). This developer now has to guess what the developer who originally implemented the code was thinking and how they decided to implement the code that accesses these assets. Frustrating, right? Not to mention it’s unproductive. Not the best DevX.
4. Refactors become a nightmare
Scenarios 1 and 3 can happen in isolation, but those scenarios become particularly more critical and recurring when the project needs to undergo some sort of refactor, be it an architecture change, or a file structure re-organization, simply refactoring files from UIKit to SwiftUI, or from Objective-C to Swift, etc. Each change you make you need to be more careful, double and triple check your changes, double down on review, double down on manual exploration testing (the most expensive type of tests), just to ensure that everything will go out smoothly. Whereas, if the project (and the team) followed the convention of never initializing images using dynamic strings, certain refactors would be a lot smoother.
Needless to say I’m a big fan of codebase standardization, consistency, and Convention Over Configuration. 🤓
Meta-programming kicks in
Of course. I couldn’t write this article without mentioning meta-programming, and amazing developer tools like SwiftGen and Sourcery.
Meta-programming helps not only reduce the pain of manually writing out boilerplate code, but in this case the most important benefit is making your images compile-time safe. Without digging into its implementation detail (there are a bunch of articles explaining just that e.g. the official one from SwiftGen, this one from Kodeco (former Ray Wenderlich) and this other one), your code could become something like this:
enum Direction {
case left, right, up, down
var icon: UIImage {
switch self {
case .left: return UIImage.Icons.Direction.left
case .right: return UIImage.Icons.Direction.right
case .up: return UIImage.Icons.Direction.up
case .down: return UIImage.Icons.Direction.down
}
}
And, in case anything goes wrong with your asset clean up or refactor, your code won’t compile anymore and you will know exactly where things went wrong. Typos in asset names will be a thing from the past, and developers can easily find where the assets are being consumed (in the generated files, by searching like they normally would).
Exceptions
My image identifiers come from the backend, and my images are stored locally
Here I’d say it depends on the quantity. I would probably avoid having backend-driven image identifiers and locally-stored images if possible, but if that’s not the best approach for your project, I’d probably still declare images explicitly (without using dynamic strings) if there’s a reasonable number of images to be mapped. If there are thousands of them, then probably not. 🙃
It’s too complicated to use meta-programming
Believe me, it’s not! But, of course, if you’re an indie developer bootstrapping a pet project, or if it’s a small project with 10-20 images, it’s okay to declare them using string literals. But as the project grows, the advantages of having your image references compile-time safe start to stand out.
Conclusion
Keeping your image initialization code static has multiple benefits:
Code safety
Smoother usage of tools that identify and clean up unused assets
It’s more productive to have your codebase follow a single standard, contributing to your team’s DevX
Keep your future self (or colleagues) sane, by making refactors easier
If you or your team is still using dynamic strings in your codebase, consider if that’s really the best approach for your project. Maybe you’ve already faced some of the issues highlighted in this article.
And if you have legitimate use cases to initialize UIImages using dynamic strings, reach out to me on @rogerluan_, I’d love to learn more!