đźď¸ 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!