Sign in with GitHub to watch this episode for free.

Code Smells in Swift

Stringly Typed Code

1 Meaningless Fallback Values 06:54
2 Stringly Typed Code 06:24

A magic number is a number that is used in code without much meaning due to the lack of context. Magic numbers are considered an anti-pattern because they make code harder to read, understand, and maintain. Refactoring code that uses magic numbers is also more risky and can be time-consuming.

String literals can introduce similar problems. Why is that? A string literal can result in stringly typed code. If a string is used instead of a more appropriate type, then we speak of stringly typed code.

I tend to avoid string literals as much as possible, especially if that string literal is used in several places. The good news is that you can avoid string literals with little effort. In this episode of Code Smells in Swift, I show you a few solutions to avoid string literals and stringly typed code.

String Literals

Let's take a look at a few examples to understand the problem. This is a common use case of string literals. To create a UIImage instance from an asset in the asset catalog, we pass the name of the asset to one of the initializers of the UIImage class. Because the initializer accepts a string, this seems like a valid use case for string literals. Right?

func imageForIcon(withName name: String) -> UIImage? {
    switch name {
    case "clear-day":
        return UIImage(named: "clear-day")
    case "clear-night":
        return UIImage(named: "clear-night")
    case "rain":
        return UIImage(named: "rain")
    case "snow":
        return UIImage(named: "snow")
    case "sleet":
        return UIImage(named: "sleet")
    case "wind",
         "cloudy",
         "partly-cloudy-day",
         "partly-cloudy-night":
        return UIImage(named: "cloudy")
    default:
        return UIImage(named: "clear-day")
    }
}

What happens if you make a typo? The compiler won't complain because it doesn't check that the name you pass to the initializer matches an asset in the asset catalog. If you make a typo, the initialization fails silently and the application most likely renders an empty image view. Bugs like this can live in an application for quite some time before they show up on your radar.

Let's take a look at another example. The identifier of a storyboard segue is a string. You define the identifier of the segue in the storyboard and reference it in code. This seems like another valid use case for string literals. The repercussions of a typo can be more dramatic in this scenario.

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

    switch identifier {
    case "SegueDayView":
    	...
    case "SegueWeekView":
    	...
    case "SegueSettingsView":
    	...
    case "SegueLocationsView":
    	...
    default: break
    }
}

The good news is that we can avoid string literals in both examples with a simple solution. Let's start with the segue identifiers. We define a private enum with name Segue. For each segue, we declare a static constant with a descriptive name. The value of the static constant is the segue identifier.

private enum Segue {
    static let dayView = "SegueDayView"
    static let weekView = "SegueWeekView"
    static let settingsView = "SegueSettingsView"
    static let locationsView = "SegueLocationsView"
}

We can use the Segue enum in the prepare(for:sender:) method, eliminating the need for string literals.

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

    switch identifier {
    case Segue.dayView:
        ...
    case Segue.weekView:
        ...
    case Segue.settingsView:
        ...
    case Segue.locationsView:
        ...
    default: break
    }
}

You may argue that we moved the string literals from the prepare(for:sender:) method to the Segue enum. That is true, but we no longer need to worry about typos in the prepare(for:sender:) method. The string literals are defined in a single location, the compiler ensures we don't introduce typos, and we get auto-completion for free. We can take it one step further, though.

Let's revisit the Segue enum. We define the raw values of the Segue enum to be of type String. We no longer use static constants. Each segue is defined by a case. The raw value of the case is the identifier of the segue.

private enum Segue: String {
    case day = "SegueDayView"
    case week = "SegueWeekView"
    case settings = "SegueSettingsView"
    case locations = "SegueLocationsView"
}

In the prepare(for:sender:) method, we use the identifier of the segue to create a Segue object. The initialization of the Segue object fails if the identifier of the segue shouldn't be handled by the prepare(for:sender:) method.

The added benefit of this solution is that we can switch on the Segue object. The switch statement no longer needs a default case and the compiler throws an error if we forget to handle a segue we should handle.

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

    switch storyboardSegue {
    case .day:
        ...
    case .week:
        ...
    case .settings:
        ...
    case .locations:
        ...
    }
}

Code Generation

We can use a similar solution for creating a UIImage instance from an asset in the asset catalog. A more scalable solution is to use a solution like SwiftGen. SwiftGen inspect the project's resources and generates the required code every time the project is built.

This has a number of benefits. Let me illustrate this with an example. I won't cover setting up SwiftGen in this episode. I already installed and configured SwiftGen for this project. We can leverage the code generated by SwiftGen to update the imageForIcon(withName:) method. SwiftGen generated an enum with name Assets. The Assets enum defines a case for each asset in the asset catalog. We create a UIImage instance through the computed image property. That's it.

func imageForIcon(withName name: String) -> UIImage? {
    switch name {
    case "clear-day":
        return Assets.clearDay.image
    case "clear-night":
        return Assets.clearNight.image
    case "rain":
        return Assets.rain.image
    case "snow":
        return Assets.snow.image
    case "sleet":
        return Assets.sleet.image
    case "wind",
         "cloudy",
         "partly-cloudy-day",
         "partly-cloudy-night":
        return Assets.cloudy.image
    default:
        return Assets.clearDay.image
    }
}

What happens if we remove an asset from the asset catalog? Because the generated code is updated every time the project is built, the compiler throws an error. We are notified that the project uses an asset that is no longer present in the asset catalog. This is useful for detecting missing assets, but it is also useful to clean up resources without running into surprises.

Avoiding Stringly Typed Code in Swift

SwiftGen supports a variety of resources, including asset catalogs and localizable strings. You no longer need to worry about typos and auto-completion makes it quick and easy. You can drastically reduce your use of string literals with a tool like SwiftGen.

What's Next?

I consider random string literals a code smell just like magic numbers are a code smell. It takes little effort to avoid string literals and the solutions I shared in this episode are simple and have several benefits. The next time you are about to use a string literal, consider if there are other options.