String literals in Swift
Basics article available: StringsBeing able to express basic values, such as strings and integers, using inline literals is an essential feature in most programming languages. However, while many other languages have the support for specific literals baked into their compiler, Swift takes a much more dynamic approach — using its own type system to define how various literals should be handled, through protocols.
This week, let’s focus on string literals in particular, by taking a take a look at the many different ways that they can be used and how we — through Swift’s highly protocol-oriented design — are able to customize the way literals are interpreted, which lets us do some really interesting things.
The basics
Just like in many other languages, Swift strings are expressed through literals surrounded by quotation marks — and can contain both special sequences (such as newlines), escaped characters, and interpolated values:
let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"
// John says "Hi!"
// Would you like to reply?
While the features used above already provide us with a lot of flexibility, and are most likely enough for the vast majority of use cases, there are situations in which more powerful ways of expressing literals can come in handy. Let’s take a look at some of those, starting with when we need to define a string containing multiple lines of text.
Multiline literals
Although any standard string literal can be broken up into multiple lines using \n
, that’s not always practical — especially if we’re looking to define a larger piece of text as an inline literal.
Thankfully, since Swift 4, we’re also able to define multiline string literals using three quotation marks instead of just one. For example, here we’re using that capability to output a help text for a Swift script, in case the user didn’t pass any arguments when invoking it on the command line:
// We're comparing against 1 here, since the first argument passed
// to any command line tool is the current path of execution.
guard CommandLine.arguments.count > 1 else {
print("""
To use this script, pass the following:
- A string to process
- The maximum length of the returned string
""")
// Exit the program with a non-zero code to indicate failure.
exit(1)
}
Above we make use of the fact that multiline string literals preserve their text’s indentation, relative to the terminating set of quotation marks, at the bottom. They also enable us to much more freely use unescaped quotation marks within them, since they are defined by a set of three quotation marks, making the bounds of the literal much less likely to become ambiguous.
Both of the above two characteristics make multiline literals a great tool for defining inline HTML — for example in some form of web page generation tool, or when rendering parts of an app’s content using web views — like this:
extension Article {
var html: String {
// If we want to break a multiline literal into separate
// lines without causing an *actual* line break, then we
// can add a trailing '\' to one of our lines.
let twitterLink = """
<a href="https://twitter.com/\(author.twitterHandle)">\
@\(author.twitterHandle)</a>
"""
return """
<article>
<h1>\(title)</h1>
<div class="author">
<p>\(author.name)</p>
\(twitterLink)
</div>
<div class="body">\(body)</div>
</article>
"""
}
}
The above technique can also be really useful when defining string-based test data. For example, let’s say that our app’s settings need to be exportable as XML, and that we want to write a test that verifies that functionality. Rather than having to define the XML that we want to verify against in a separate file — we can use a multiline string literal to inline it into our test:
class SettingsTests: XCTestCase {
func testXMLConversion() {
let settings = Settings(
messageLimit: 7,
enableSync: true,
signature: "Sent from my Swift app"
)
XCTAssertEqual(settings.xml, """
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<messagelimit>7</messagelimit>
<enablesync>1</enablesync>
<signature>Sent from my Swift app</signature>
</settings>
""")
}
}
The benefit of defining test data inline, like we do above, is that it becomes much easier to quickly spot any errors made when writing the test — since the test code and the expected output are placed right next to each other. However, if some test data exceeds a handful of lines in length, or if the same data needs to be used in multiple place, it can still be worth moving it to its own file.
Raw strings
New in Swift 5, raw strings enable us to turn off all dynamic string literal features (such as interpolation, and interpreting special characters, like \n
), in favor of simply treating a literal as a raw sequence of characters. Raw strings are defined by surrounding a string literal with pound signs (or “hashtags”, as the kids call them):
let rawString = #"Press "Continue" to close this dialog."#
Just like how we above used a multiline literal to define test data, raw string literals are particularly useful when we want to inline strings that need to contain special characters — such as quotation marks or backslashes. Here’s another test-related example, in which we use a raw string literal to define a JSON string to encode a User
instance from:
class UserTests: XCTestCase {
func testDecoding() throws {
let json = #"{"id": 37, "name": "John"}"#
let data = Data(json.utf8)
let user = try data.decoded() as User
XCTAssertEqual(user.id, 37)
XCTAssertEqual(user.name, "John")
}
}
Above we use the type inference-based decoding API from “Type inference-powered serialization in Swift”.
While raw strings disable features like string interpolation by default, there is a way to override that by adding another pound sign right after the interpolation’s leading backslash — like this:
extension URL {
func html(withTitle title: String) -> String {
return #"<a href="\#(absoluteString)">\#(title)</a>"#
}
}
Finally, raw strings are also particularly useful when interpreting a string using a specific syntax, especially if that syntax relies heavily on characters that would normally need to be escaped within a string literal — such as regular expressions. By defining regular expressions using raw strings, no escaping is needed, giving us expressions that are as readable as they get:
// This expression matches all words that begin with either an
// uppercase letter within the A-Z range, or with a number.
let regex = try NSRegularExpression(
pattern: #"(([A-Z])|(\d))\w+"#
)
Even with the above improvements, it’s questionable how easy to read (and debug) regular expressions are — especially when used in the context of a highly type-safe language like Swift. It’ll most likely come down to any given developer’s previous experience with regular expressions, whether or not they prefer them over implementing more custom string parsing algorithms, directly in Swift.
Expressing values using string literals
While all string literals are turned into String
values by default, we can also use them to express custom values as well. Like we took a look at in “Type-safe identifiers in Swift”, adding string literal support to one of our own types can let us achieve increased type safety, without sacrificing the convenience of using literals.
For example, let’s say that we’ve defined a Searchable
protocol to act as the API for searching any kind of database or underlying storage that our app uses — and that we’re using a Query
enum to model different ways to perform such a search:
protocol Searchable {
associatedtype Element
func search(for query: Query) -> [Element]
}
enum Query {
case matching(String)
case notMatching(String)
case matchingAny([String])
}
The above approach gives us a lot of power and flexibility as to how we’ll perform each search, but the most common use case is still likely to be the simplest one — searching for elements matching a given string — and it would be really nice if we were able to do that using a string literal.
The good news is that we can make that happen, while still keeping the above API completely intact, by making Query
conform to ExpressibleByStringLiteral
:
extension Query: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self = .matching(value)
}
}
That way we’re now free to perform matching searches without having to create a Query
value manually — all we need to do is pass a string literal as if the API we’re calling actually accepted a String
directly. Here we’re using that capability to implement a test that verifies that a UserStorage
type correctly implements its search functionality:
class UserStorageTests: XCTestCase {
func testSearch() {
let storage = UserStorage.inMemory
let user = User(id: 3, name: "Amanda")
storage.insert(user)
let matches = storage.search(for: "anda")
XCTAssertEqual(matches, [user])
}
}
Custom string literal expressions can in many situations let us avoid having to pick between type safety and convenience when working with string-based types, such as queries and identifiers. It can be a great tool to use in order to achieve an API design that scales well from the simplest use case, all the way to covering edge cases and offering more power and customizability when needed.
Custom interpolation
One thing that all “flavors” of Swift string literals have in common is their support for interpolating values. While we’ve always been able to customize how a given type is interpolated by conforming to CustomStringConvertible
— Swift 5 introduces new ways of implementing custom APIs right on top of the string interpolation engine.
As an example, let’s say that we want to save a given string by optionally applying a prefix and suffix to it. Ideally we’d like to simply interpolate those values to form the final string, like this:
func save(_ text: String, prefix: String?, suffix: String?) {
let text = "\(prefix)\(text)\(suffix)"
textStorage.store(text)
}
However, since both prefix
and suffix
are optionals, simply using their description won’t produce the result we’re looking for — and the compiler will even give us a warning:
String interpolation produces a debug description for an optional value
While we always have the option of unwrapping each of those two optionals before interpolating them, let’s take a look at how we could do both of those things in one go using custom interpolation. We’ll start by extending String.StringInterpolation
with a new appendInterpolation
overload that accepts any optional value:
extension String.StringInterpolation {
mutating func appendInterpolation<T>(unwrapping optional: T?) {
let string = optional.map { "\($0)" } ?? ""
appendLiteral(string)
}
}
The above unwrapping:
parameter label is important, as it’s what we’ll use to tell the compiler to use that specific interpolation method — like this:
func save(_ text: String, prefix: String?, suffix: String?) {
let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)"
textStorage.store(text)
}
Although it’s just syntactic sugar, the above looks really neat! However, that barely scratches the surface of what custom string interpolation methods can do. They can be both generic and non-generic, accept any number of arguments, use default values, and pretty much anything else that “normal” methods can do.
Here’s another example in which we enable our method for converting a URL into an HTML link from before to also be used in the context of string interpolation:
extension String.StringInterpolation {
mutating func appendInterpolation(linkTo url: URL,
_ title: String) {
let string = url.html(withTitle: title)
appendLiteral(string)
}
}
With the above in place, we can now easily generate HTML links from a URL like this:
webView.loadHTMLString(
"If you're not redirected, \(linkTo: url, "tap here").",
baseURL: nil
)
The cool thing about custom string interpolation is how the compiler takes each of our appendInterpolation
methods and translates them into corresponding interpolation APIs — giving us complete control over what the call site will look like, for example by removing external parameter labels, like we did for title
above.
We’ll continue looking into more ways of using custom string interpolation, for example with attributed strings and other kinds of text metadata, in upcoming articles.
Conclusion
While some of Swift’s more advanced string literal capabilities are only really useful in very specific situations, such as the ones in this article, it’s nice to have them available when needed — especially since it’s possible to completely avoid them and only use strings "the old-fashioned way"
.
String literals is another area in which Swift’s protocol-oriented design really shines. By delegating much of how literals are interpreted and handled to implementors of protocols, rather than hard-coding those behaviors in the compiler itself, we as third-party developers are able to heavily customize the way literals are handled — while still keeping the defaults as simple as they can be.
What do you think about string literals and the new APIs introduced in Swift 5? Let me know — along with your questions, comments and feedback — either on Twitter or by contacting me.
Thanks for reading! 🚀