String Interpolation
Swift 5 added some nice improvements to string interpolation that many people may not be aware of.
The first is the ability to control interpolation into a custom type that adopts ExpressibleByStringInterpolation
. This allows you to create types like HTML
and drives the new OSLog string formatting (eg log.debug("value \(x, privacy: .public)")
. Becca Royal-Gordon provided some nice examples of this in the SE-0228 proposal.
As a side-effect String
and Substring
now use DefaultStringInterpolation
giving us a convenient extension point to customize interpolation behavior.
DefaultStringInterpolation
As an example let's say we want to represent file URLs as paths without the file://
prefix. We can accomplish this easily:
extension DefaultStringInterpolation {
fileprivate mutating func appendInterpolation(_ url: URL) {
if url.isFileURL {
appendInterpolation(url.path)
} else {
// Careful, passing `url` here creates
// an eternal loop that smashes the stack
appendInterpolation(url.description)
}
}
}
let u = URL(fileURLWithPath: "/private", isDirectory: true)
print("\(u)") //prints "/private" instead of "file:///private"
A good practice is to ensure that all paths in your appendInterpolation
method ultimately pass a StringProtocol
type onward to avoid accidental eternal loops or ensure your first parameter is named so your method is not chosen as a default overload.For framework authors you should consider any public DefaultStringInterpolation
extensions very carefully.
For another example assume we'd like to have a convenient way to format integers:
enum IntegerStyle {
case decimal
case hexadecimal
case octal
}
fileprivate enum BinaryIntegerCategory { case signed, unsigned }
fileprivate extension BinaryInteger {
var interpolationValue: (BinaryIntegerCategory, UInt64) {
return (self is any SignedInteger) ?
(.signed, UInt64(bitPattern: Int64(self))) :
(.unsigned, UInt64(self))
}
}
extension DefaultStringInterpolation {
mutating func appendInterpolation<Value: BinaryInteger>(
_ value: Value,
style: IntegerStyle
) {
let (category, value) = value.interpolationValue
switch style {
case .decimal where category == .signed:
appendInterpolation(String(format: "%lld", value))
case .decimal:
appendInterpolation(String(format: "%llu", value))
case .hexadecimal:
appendInterpolation(String(format: "0x%llx", value))
case .octal:
appendInterpolation(String(format: "0o%llo", value))
}
}
}
Values are promoted to 64-bit so our value is a CVarArg
and we only need one size prefix in our format string. Signed decimal is the only special case but we only need to use the correct format character. All integer values are promoted to 64-bit when passed as varargs anyway and the underlying printf
library will interpret those 64-bits entirely based on the argument length modifier and conversion specifier.
This example could be further extended to support floats, alignment, minimum widths, and various other features of printf-style formatting.
This blog represents my own personal opinion and is not endorsed by my employer.