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.
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)".
With this implementation we can write code which looks like this:
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.
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.
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.
SwiftStringInterpolationPlayground (Xcode 12.4)