UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to use custom string interpolation

Control how types look in strings, or create them from scratch.

Paul Hudson       @twostraws

SE-0228 dramatically revamped Swift’s string interpolation system so that it’s more efficient and more flexible, and it’s creating a whole new range of features that were previously impossible.

I’ve already written extensively about the other great features coming in Swift 5, including raw strings, the Result type, @dynamicCallable, and more, but in this article I want to walk you through what’s changing in string interpolation and how you can use this new functionality in your own code.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Interpolation how you want

In its most basic form, the new string interpolation system lets us control how objects appear in strings. Swift has default behavior for structs that is helpful for debugging, because it prints the struct name followed by all its properties. But if you were working with classes (that don’t have this behavior), or wanted to format that output so it could be user-facing, then you could use the new string interpolation system.

For example, if we had a struct like this:

struct User {
    var name: String
    var age: Int
}

If we wanted to add a special string interpolation for that so that we printed users neatly, we would add an extension to String.StringInterpolation with a new appendInterpolation() method. Swift already has several of these built in, and users the interpolation type – in this case User to figure out which method to call.

In this case, we’re going to add an implementation that puts the user’s name and age into a single string, then calls one of the built-in appendInterpolation() methods to add that to our string, like this:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: User) {
        appendInterpolation("My name is \(value.name) and I'm \(value.age)")
    }
}

Now we can create a user and print out their data:

let user = User(name: "Guybrush Threepwood", age: 33)
print("User details: \(user)")

That will print User details: My name is Guybrush Threepwood and I'm 33, whereas with the custom string interpolation it would have printed User details: User(name: "Guybrush Threepwood", age: 33). Of course, that functionality is no different from just implementing the CustomStringConvertible protocol, so let’s move on to more advanced usages.

Your custom interpolation method can take as many parameters as you need, labeled or unlabeled. For example, we could add an interpolation to print numbers using various styles, like this:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
        let formatter = NumberFormatter()
        formatter.numberStyle = style

        if let result = formatter.string(from: number as NSNumber) {
            appendLiteral(result)
        }
    }
}

The NumberFormatter class has a number of styles, including currency ($72.83), ordinal (1st, 12th), and spell out (five, forty-three). So, we could create a random number and have it spelled out into a string like this:

let number = Int.random(in: 0...100)
let lucky = "The lucky number this week is \(number, style: .spellOut)."
print(lucky)

You can call appendLiteral() as many times as you need, or even not at all if necessary. For example, we could add a string interpolation to repeat a string multiple times, like this:

extension String.StringInterpolation {
    mutating func appendInterpolation(repeat str: String, _ count: Int) {
        for _ in 0 ..< count {
            appendLiteral(str)
        }
    }
}

print("Baby shark \(repeat: "doo ", 6)")

And, as these are just regular methods, you can use Swift’s full range of functionality. For example, we might add an interpolation that joins an array of strings together, but if that array is empty execute a closure that returns a string instead:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
        if values.count == 0 {
            appendLiteral(defaultValue())
        } else {
            appendLiteral(values.joined(separator: ", "))
        }
    }
}

let names = ["Harry", "Ron", "Hermione"]
print("List of students: \(names, empty: "No one").")

Using @autoclosure means that we can use simple values or call complex functions for the default value, but none of that work will be done unless values.count is zero.

Constructing types from string interpolation

Using a combination of the ExpressibleByStringLiteral and ExpressibleByStringInterpolation protocols it’s now possible to create whole types using string interpolation, and if we add CustomStringConvertible we can even make those types print as strings however we want.

To make this work, we need to fulfill some specific criteria:

  • Whatever type we create should conform to ExpressibleByStringLiteral, ExpressibleByStringInterpolation, and CustomStringConvertible. The latter is only needed if you want to customize the way the type is printed.
  • Inside your type needs to be a nested struct called StringInterpolation that conforms to StringInterpolationProtocol.
  • The nested struct needs to have an initializer that accepts two integers telling us roughly how much data it can expect.
  • It also needs to implement an appendLiteral() method, as well as one or more appendInterpolation() methods.
  • Your main type needs to have two initializers that allow it to be created from string literals and string interpolations.

I know that all sounds like a lot, but it’s all there for a reason:

  • Having a nested struct means your main type doesn’t get cluttered up with all the implementation details of string interpolation.
  • It also allows you to have one or more properties inside that nested struct storing temporary data while your data is being assembled. The Swift Evolution proposal for this change likens it to a scratchpad.
  • Having an initializer telling us how much data to expect allows us to preallocate space for however much space we think our final data might be.
  • Having two initializers in the main type lets us handle both string literals (“hello, world!”) and string interpolation ("hello, \(name)!").

We can put all that together into an example type that can construct HTML from various common elements. The “scratchpad” inside the nested StringInterpolation struct will be a string: each time a new literal or interpolation is added, we’ll append it to the string. To help you see exactly what’s going on, I’ve added some print() calls inside the various append methods.

Here’s the code.

struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
    struct StringInterpolation: StringInterpolationProtocol {
        // start with an empty string
        var output = ""

        // allocate enough space to hold twice the amount of literal text
        init(literalCapacity: Int, interpolationCount: Int) {
            output.reserveCapacity(literalCapacity * 2)
        }

        // a hard-coded piece of text – just add it
        mutating func appendLiteral(_ literal: String) {
            print("Appending \(literal)")
            output.append(literal)
        }

        // a Twitter username – add it as a link
        mutating func appendInterpolation(twitter: String) {
            print("Appending \(twitter)")
            output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
        }

        // an email address – add it using mailto
        mutating func appendInterpolation(email: String) {
            print("Appending \(email)")
            output.append("<a href=\"mailto:\(email)\">\(email)</a>")
        }
    }

    // the finished text for this whole component
    let description: String

    // create an instance from a literal string
    init(stringLiteral value: String) {
        description = value
    }

    // create an instance from an interpolated string
    init(stringInterpolation: StringInterpolation) {
        description = stringInterpolation.output
    }
}

We can now create and use an instance of HTMLComponent using string interpolation like this:

let text: HTMLComponent = "You should follow me on Twitter \(twitter: "twostraws"), or you can email me at \(email: "paul@hackingwithswift.com")."
print(text)

Thanks to the print() calls that were scattered inside, you’ll see exactly how the string interpolation functionality works: you’ll see “Appending You should follow me on Twitter”, “Appending twostraws”, “Appending , or you can email me at “, “Appending paul@hackingwithswift.com”, and finally “Appending .” – each part triggers a method call, and is added to our string.

Where next?

This is just one several new features in Swift 5. If you’d like to learn about the others, see my article what’s new in Swift 5, or my whole site dedicated to tracking what’s new in Swift.

If you’d like to learn more about the new string interpolation functionality, there is some further reading I can highly recommend – they really helped clarify for me how this feature works, what its goals are, and how it might be used productively:

If you’re curious to see folks using this functionality for real, check out Ilya Puchka’s Interplate repository on GitHub – it’s a Swift templating library built using Swift 5.0 string interpolation.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 3.9/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.