The Danger of String Literals and Stringly Typed Code

Object literals are very useful and they often make your code easier to read and understand. String literals are a bit different, though. It's true that a string literal is the easiest solution to create a String instance. It's straightforward and everyone understands what's going on. But there's a price you pay every time you use a string literal. Did you know that?

Common Examples

Let me show you a few common examples of string literals as they're used in Swift and Cocoa development.

let image = UIImage(named: "icon_button_add")
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let text = NSLocalizedString("profile_view_button_add", comment: "Profile View Add Button")
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let identifier = segue.identifier else { return }

    switch identifier {
    case "Profiles":
        guard let destination = segue.destination as? ProfilesViewController else {
            return
        }

        ...
    default: break
    }
}

I bet these examples look familiar. In each example, we use one or more string literals to carry out a task. What's wrong with these examples?

While there's nothing inherently wrong with the use of string literals, there's room for improvement. String literals very often lead to stringly typed code, a play on strongly typed.

What Is Stringly Typed Code

When strings, and very often string literals, are used instead of a more appropriate type, we speak of stringly typed code. Some forms of stringly typing are obvious while others are more subtle. This is an obvious example of stringly typed code.

func fetchWeatherDataFor(latitude: String, longitude: String, completionHandler: @escaping (JSON) -> Void) {
    ...
}

When working with coordinates, there's a better solution than using strings for the location's latitude and longitude. This is a better implementation that adds safety and clarity.

func fetchWeatherDataFor(latitude: Double, longitude: Double, completionHandler: @escaping (JSON) -> Void) {

}

We can take it one step further by asking the Core Location framework for help.

func fetchWeatherData(for location: CLLocation, completionHandler: @escaping (JSON) -> Void) {

}

Another obvious example is the use of a String instance instead of a URL instance. There's a reason why Apple pushes APIs that accept a URL instance instead of a path, that is, a String instance.

Subtle Forms of Stringly Typed Code

There are many subtle forms of stringly typed code that are often overlooked. These forms are easy to avoid with a tiny amount of refactoring. Let me show you how I get rid of the string literals in the examples I showed you earlier.

Images

Whenever I use an API that accepts a String instance, I'm on my guard. Why? It's an opportunity for a string literal to make its way into my codebase. This is a very common example.

let image = UIImage(named: "icon_button_add")

This is how the majority of developers create UIImage instances. The UIImage class explicitly asks for a String instance. What's wrong with this? And how can we improve this example?

Let me ask you a few questions first. What happens if the asset's name changes? And what happens if you misspell the asset's name? Because init(named:) returns an optional UIImage instance, the compiler won't warn you if there isn't an asset with the name you passed to init(named:). How can we solve this problem?

There are several solutions. I usually create an extension for UIImage that looks something like this.

import UIKit

extension UIImage {

    enum Icons {

        enum Button {

            static let Add = UIImage(named: "icon_button_add")!

        }

    }

}

Does this look overly complex to you? I can understand that it may look too complex of a solution for the problem. Bear with me, though. This solution makes clever use of enumerations to create namespaces. It's a pattern I use very, very often and it's one of the patterns I swear by.

The resulting API, however, looks and feels elegant and intuitive. Don't you agree? This is how it looks at the call site.

let image = UIImage.Icons.Button.Add

Notice that I use an exclamation mark in the UIImage extension to forced unwrap the result of init(named:). As I write elsewhere, I rarely use Swift's exclamation mark. One exception is the loading of assets and resources from the application's bundle. Such an operation should never fail. If it fails, then I have other things to worry about and the application crashing due to a runtime error is the least of my worries.

static let Add = UIImage(named: "icon_button_add")!

You may be wondering if instantiating a UIImage instance in an extension is a wise thing to do. Don't worry, though. Static properties like this are instantiated lazily. That's obviously a good thing since we want to load an asset the moment it's needed. This improves performance and reduces memory consumption.

If you're worried about memory consumption, then you can turn the static constant property into a static computed property.

import UIKit

extension UIImage {

    enum Icons {

        enum Button {

            static var Add: UIImage {
                return UIImage(named: "icon_button_add")!
            }

        }

    }

}

Storyboards

I use a similar solution for loading storyboards and instantiating the view controllers they contain. I create an extension for UIStoryboard and define a static constant property.

import UIKit

extension UIStoryboard {

    static let main = UIStoryboard(name: "Main", bundle: Bundle.main)

}

It's important to realize that the storyboard is kept in memory for the lifetime of the application once it's instantiated. If that isn't what you want, then you can turn the static constant property into a static computed property.

import UIKit

extension UIStoryboard {

    static var main: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }

}

A new instance of the storyboard is created every time the main computed property is accessed. You can use a similar strategy for the view controllers of a storyboard. Take a look at the following example and notice the use of the exclamation mark.

import UIKit

extension UIStoryboard {

    // MARK: - Storyboards

    static var main: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }

    static var onboarding: UIStoryboard {
        return UIStoryboard(name: "Onboarding", bundle: Bundle.main)
    }

    // MARK: - View Controllers

    static var onboardingViewController: OnboardingViewController {
        return UIStoryboard.onboarding.instantiateInitialViewController() as! OnboardingViewController
    }

}

An added benefit is that the pattern makes the instantiation of the OnboardingViewController class more concise at the call site.

let onboardingViewController = UIStoryboard.onboardingViewController

This is much better than the alternative, especially if you need to instantiate the view controller in multiple locations.

let storyboard = UIStoryboard(name: "Onboarding", bundle: Bundle.main)

guard let onboardingViewController = storyboard.instantiateInitialViewController() as? OnboardingViewController else {
    ...
}

Localization

You can use a similar strategy for localization. It's unfortunate that Apple doesn't add support for this in Xcode. I'd be nice to take advantage of autocompletion when localizing an application.

Segues

The last example that's worth mentioning is the use of segues. The approach I'm currently using is simple yet effective. I define a private enum, Segue, in the view controller. For every segue of the view controller, I define a static constant property. This means I no longer need to deal with string literals or constant stored properties.

class ViewController: UIViewController {

    // MARK: - Segues

    private enum Segue {

        static let Profiles = "Profiles"

    }

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let identifier = segue.identifier else { return }

        switch identifier {
        case Segue.Profiles:
            guard let destination = segue.destination as? ViewController else {
                return
            }

            ...
            break
        default: break
        }
    }

}

Naming Conventions

You may have noticed that I sometimes capitalize the names of the static properties I define. This is a personal choice that's largely inspired by the Cocoa SDK. The rule I apply in my projects is simple. If the static property is a property of its type, then I don't capitalize the property's name.

import UIKit

extension UIStoryboard {

    static var main: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }

}

I capitalize the property's name if the static property is a property of, for example, an enum. This naming convention adds clarity and a bit of context.

What Do We Gain

The Xcode theme I use highlights string literals in bright red. It shows me an overview of the string literals I'm using and, when I see too much red, it's time for some refactoring.

This is the Xcode theme I use.

But you may be wondering why this is necessary. I hope I've convinced you that the use of string literals can break your application without the compiler warning you about it. That's the inherent risk of string literals and stringly typed code.

The solutions I outlined in this tutorial come with a few key benefits. You can take advantage of Xcode's autocompletion and, more importantly, the compiler helps you find common bugs that are the result of typos. Refactoring also becomes easier and less prone to errors. You don't need to rely on search and replace to update the project.

Automation

Even if you use the solutions I outlined in this tutorial, you still need to be careful. For example, you need to make sure that the identifier of a segue matches the one you use in your codebase. And that's where automation can prove very useful.

There are several libraries and frameworks that automatically generate the extensions I defined in this tutorial. The code generation usually takes place at compile time. This means that you eliminate typos, benefit from code completion, and, when you make a change, it's automatically propagated.

I currently don't use one of these solutions, but I have to admit that it's very tempting if you care about safety and consistency. To be honest, I believe it's Xcode's responsibility to handle such tasks. Many popular IDEs take care of these cumbersome tasks.

Loading an image from an asset catalog, for example, should be easier and less prone to errors. It shouldn't involve string literals if the image is included in the application't bundle. It could look something like this.

Assets(bundle: .main).iconButtonAdd