Swift 5 introduced a decent version of string interpolation with SE-228 and updated the old version of the ExpressibleByStringInterpolation
protocol that has been deprecated since Swift 3. The new version of this protocol allows us to write powerful extensions to existing types that define how objects interact when used within string interpolation.
While developing apps we’re oftentimes printing out data that helps us understand our apps during testing or debugging. Using Custom debug descriptions to improve debugging is one way of improving the way we work with strings in Swift but we can reach another level by combining this with making use of the advantages of string interpolation.
What is string interpolation?
The subject is quite self-explaining – it’s all about interpolating values into strings. In other words, adding a string to another string.
The simplest example of this looks as followed:
let name = "Antoine van der Lee"
print("Hello, I'm \(name)")
In this example, we’ve interpolated the name into the print statement. This results in a printed sentence “Hello, I’m Antoine van der Lee”. Underneath, we’ve made use of the DefaultStringInterpolation
structure which handles interpolating values by default and added the name to the “Hello, I’m ” string value.
Swift allows us to extend this default behavior by adding our own custom string interpolation logic for specific types, so let’s go over 4 useful cases you can use during the day to day development.
Case 1: Printing optionals
Printing optionals in Swift can be quite painful. There are several options you can take:
var title: String? = "SwiftLee - A blog about Swift"
// Force unwrapping, which has the downside of creating a potential crash if the value isn't set.
print("The title is \(title!)")
// Using a `if let` or `guard` statement to safely unwrap the value before using it.
if let unwrappedTitle = title {
print("The title is \(unwrappedTitle)")
}
// Providing a default value:
print("The title is \(title ?? "")")
// Making use of the `String(describing:)` initialiser, resulting in 'The title is Optional("SwiftLee - A blog about Swift")'
print("The title is \(String(describing: title))")
The latter is often suggested by the compiler:
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
– Use
String(describing:)
to silence this warning
I’ve been using this in projects which resulted in cluttered outputs with unuseful “Optional()” keywords in my logs. In many cases, I ended up using the default value approach which doesn’t always result in clean string interpolation. Therefore, I decided to write a custom string interpolation solution that handles optional values:
extension String.StringInterpolation {
/// Prints `Optional` values by only interpolating it if the value is set. `nil` is used as a fallback value to provide a clear output.
mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T?) {
appendInterpolation(value ?? "nil" as CustomStringConvertible)
}
}
This now allows us to print out optionals as before but we no longer get the compiler suggestion.
print("The title is \(title)")
// Prints: The title is SwiftLee - A blog about Swift
The value is printed out nicely without the “Optional(..)” keyword. If the value is not set, “nil” will be printed which clearly states that the value is not set and which will help us during debugging sessions.
Case 2: Printing JSON
Another often used use-case is printing out JSON responses from data requests. This can be useful during debugging but the code to print out JSON might not always be at your hands. I’ve found myself several times searching for code on Stack Overflow to find out how I could pretty print JSON from Data
.
To solve this, I’ve created a custom string interpolation method that takes Data
as input:
extension String.StringInterpolation {
mutating func appendInterpolation(json JSONData: Data) {
guard
let JSONObject = try? JSONSerialization.jsonObject(with: JSONData, options: []),
let jsonData = try? JSONSerialization.data(withJSONObject: JSONObject, options: .prettyPrinted) else {
appendInterpolation("Invalid JSON data")
return
}
appendInterpolation("\n\(String(decoding: jsonData, as: UTF8.self))")
}
}
This interpolation method introduces a new way of working with string interpolation and requires us to set a property name inside the interpolation. This might be uncommon for you at first but it can become useful once you understand the possibilities it brings. In this case, our JSON is printed out nicely!
let jsonData = """
{
"name": "Antoine van der Lee"
}
""".data(using: .utf8)!
print("The provided JSON is \(json: jsonData)")
// Prints: The provided JSON is
// {
// "name" : "Antoine van der Lee"
// }
This can also be used directly in URLSessionDataTask
responses:
URLSession.shared.dataTask(with: URL(string: "my.api.com/endpoint")!) { (data, response, error) in
if let data = data {
print("The JSON response is \(json: data)")
}
}
Case 3: Printing URL requests
Just like with printing JSON responses it can also be useful to print out any outgoing URLRequest
. While developing apps we often make use of networking requests and it can be useful to know more about outgoing requests.
By default, a URLRequest
is printed out as follows:
var request = URLRequest(url: URL(string: "https://www.avanderlee.com/feed?offset=10&limit=20")!)
request.httpMethod = "GET"
print("The request is \(request)")
// Prints: "The request is https://www.avanderlee.com/feed?offset=10&limit=20"
Although this gives us the URL, it’s could also provide us information on the HTTP method, included headers or other useful information that you can think of.
extension String.StringInterpolation {
mutating func appendInterpolation(_ request: URLRequest) {
appendInterpolation("\(request.url) | \(request.httpMethod) | Headers: \(request.allHTTPHeaderFields)")
}
}
print("The request is \(request)")
// Prints: "The request is https://www.avanderlee.com/feed?offset=10&limit=20 | GET | Headers: [:]"
String Interpolation vs CustomStringConvertible
Although this works great, I’ve included this example to also point out that this could’ve been done by using the CustomStringConvertible
protocol instead.
The reason for pointing this out is that you have to understand that string interpolation only works if you actually combine the value inside a string.
For example, the previous example would print out the URL without extra properties if used as follows, even though we’ve configured the string interpolation extension:
print(request)
// Prints: https://www.avanderlee.com/feed?offset=10&limit=20
The reason for this is that the request is not used inside another string using the \(..)
syntax. Instead, it’s being printed out using the description
value defined in the CustomStringConvertible
protocol.
If you would like to learn more about this, you can read my blog post Using Custom debug descriptions to improve debugging.
Case 4: Converting to HTML
Whenever you’re working with HTML it might be more readable if you could make use of string interpolation. A common use-case would be to include a link in a piece of text.
By default, your String
could end up quite cluttered with HTML tags while you want to keep it readable:
"""
The blog post can be found at <a href="www.avanderlee.com">www.avanderlee.com</a>
"""
Due to the HTML <a></a>
tag we’ve added some extra syntax to our String
causing it to be less readable. Instead, we could add a custom string interpolation extension that handles adding HTML links to a String
:
extension String.StringInterpolation {
mutating func appendInterpolation(HTMLLink link: String) {
guard let url = URL(string: link) else {
assertionFailure("An invalid URL has been passed.")
return
}
appendInterpolation("<a href=\"\(url.absoluteString)\">\(url.absoluteString)</a>")
}
}
/// Can be used as follows:
"""
The blog post can be found at \(HTMLLink: "https://www.avanderlee.com")
"""
By using a custom string interpolation method we allow ourselves to add extra validation logic to make sure that a valid URL is used. We make use of an assertion failure to early on give ourselves feedback during debugging that an invalid URL has been used.
With the same idea, you could add support for markdown or other exporting methods. I’ll leave this as an exercise for you!
Conclusion
Custom string interpolation allows you to further optimize working with strings in Swift. Common use-cases like printing JSON or Optionals can make developing in Swift an even better experience. However, it’s good to know when to use custom string interpolation versus using a CustomStringConvertible
protocol implementation.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!