Custom string interpolation in Swift

ExpressibleByStringInterpolation is a protocol which makes it possible to compose strings with expressions evaluated at runtime. Interpolated strings are created with adding a \(some code) to a string. Those expressions are evaluated and a final string is created. This protocol, among other things, enables customizing strings what are created by those expressions. At the end of the post we have created a custom interpolation type which handles encodable and string representable types.

ExpressibleByStringInterpolation is a protocol which enables a type to be initialized with string interpolation. The protocol inherits from multiple other protocols, when going from top to down then it looks like this: ExpressibleByStringLiteral, ExpressibleByExtendedGraphemeClusterLiteral, and ExpressibleByUnicodeScalarLiteral. So it is a total of 4 levels of inheritance. That is important to know because if we add the protocol to a custom type then Xcode tells us about many functions the custom type needs to implement. Many of these functions already provide default implementation.

Let’s start with adding custom types Entry add EntryStorage. The storage type just keeps a collection of entries. The entry, for now, contains a string value, but we will expand the type in a way that the storage’s add function can be called with a string interpolation: storage.add("Entry (index)"). It will be very similar to OSLogMessage in Apple’s os framework.

struct EntryStorage {
private(set) var entries: [Entry] = []
mutating func add(_ entry: Entry) {
entries.append(entry)
}
}
struct Entry {
private(set) var value: String
}
view raw Interpolation.swift hosted with ❤ by GitHub

With this set, let’s add ExpressibleByStringInterpolation conformance to the Entry type along with a custom interpolation type: EntryInterpolation. ExpressibleByStringInterpolation protocol comes with an associatedtype StringInterpolation which is by default set to DefaultStringInterpolation. If we want to use custom interpolation type then we can implement the init(stringInterpolation: EntryInterpolation) with the custom type and Swift will understand that we’ll be using our own type here. No need to add typealias StringInterpolation = EntryInterpolation (although we could for clarity). The custom EntryInterpolation type needs to conform to protocol StringInterpolationProtocol. The protocol requires us to implement an init method and a appendLiteral function. The custom type will have a property for storing multiple interpolated values because it needs to represents all the expressions in a single string. For example: "Text (expression1) more text (expression2)".

struct Entry: ExpressibleByStringInterpolation {
// typealias StringInterpolation = EntryInterpolation
private(set) var value: String
init(stringLiteral value: String) {
self.value = value
}
init(stringInterpolation: EntryInterpolation) {
self.value = stringInterpolation.values.joined()
}
}
struct EntryInterpolation: StringInterpolationProtocol {
private(set) var values: [String]
init(literalCapacity: Int, interpolationCount: Int) {
self.values = []
}
mutating func appendLiteral(_ literal: StringLiteralType) {
values.append(literal)
}
}
view raw Interpolation.swift hosted with ❤ by GitHub

With this implementation we can write code which looks like this:

var entryStorage = EntryStorage()
entryStorage.add("Entry 1")
view raw Interpolation.swift hosted with ❤ by GitHub

Note that the add method takes an argument of the type Entry but here we are passing a string to the function. This works because the Entry type conforms to the ExpressibleByStringLiteral protocol which the ExpressibleByStringInterpolation includes.

Now we have basics set up and we can go and add additional functions to the EntryInterpolation type. At first, we’ll add a generic function enabling us to create interpolated strings with expressions which return a type conforming to the CustomStringConvertible protocol. There are numerous types which implement this protocol and therefore we get a support of interpolating each of those. For example, Int and Array types conform to it.

extension EntryInterpolation {
mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T) {
values.append(value.description)
}
}
let index = 2
let items = ["Item 1", "Item 2"]
entryStorage.add("Entry \(index): items=\(items)")
// Entry 2: items=["Item 1", "Item 2"]
view raw Interpolation.swift hosted with ❤ by GitHub

Sometimes we might want to pass in encodable types directly with customizable formats. Note how the interpolated expression gets a support to the custom format argument. That is because Swift converts each of the expressions to calls to appendInterpolation which can have additional arguments.

extension EntryInterpolation {
mutating func appendInterpolation<T: Encodable>(_ value: T, jsonFormat: JSONEncoder.OutputFormatting = [.prettyPrinted, .sortedKeys]) {
let encoder = JSONEncoder()
encoder.outputFormatting = jsonFormat
let data = try? encoder.encode(value)
values.append(String(data: data ?? Data(), encoding: .utf8) ?? "invalid")
}
}
struct User: Encodable {
let name: String
let age: Int
}
let user = User(name: "Appleseed", age: 20)
entryStorage.add("Entry 3: \(user, jsonFormat: .prettyPrinted)")
entryStorage.add("Entry 3: \(user, jsonFormat: .sortedKeys)")
// Entry 3: {
// "name" : "Appleseed",
// "age" : 20
//}
//Entry 3: {"age":20,"name":"Appleseed"}
view raw Interpolation.swift hosted with ❤ by GitHub

It is worth taking a look on the interface of the OSLogInterpolation type and all the appendInterpolation functions it implements. As seen so far, it is pretty easy to extend a custom interpolation type with functions like these.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

SwiftStringInterpolationPlayground (Xcode 12.4)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s