📜 ⬆️ ⬇️

Advanced string interpolation in Swift 5.0



Interpolation of lines was in Swift from earlier versions, but in Swift 5.0 this functionality was expanded, it became faster and much more powerful.

In this article, we will go through the new possibilities of string interpolation and consider how this can be applied in our own code. You can also download the source for this article here.

The basics


We use basic string interpolation like this:
')
let age = 38 print("You are \(age)") 

We take it for granted, but at one time it was a significant relief compared to what we had to deal with earlier:

 [NSString stringWithFormat:@"%ld", (long)unreadCount]; 

There is also a significant performance gain, as the alternative was:

 let all = s1 + s2 + s3 + s4 

Yes, the end result would be the same, but Swift would have to add s1 to s2 to get s5, add s5 to s3 to get s6, and add s6 to s4 to get s7, before assigning all.

Interpolation of lines practically did not change with Swift 1.0, the only significant change came with Swift 2.1, where we were able to use string literals in interpolation:

 print("Hi, \(user ?? "Anonymous")") 

As you know, Swift is developing largely due to community suggestions. Ideas are discussed, developed - and either accepted or rejected.

So, after five years, the development of Swift got to the interpolation of lines. In Swift 5.0, new super-capabilities have appeared that give us the opportunity to control the process of interpolating strings.

To try, consider the following scenario. If we set a new integer variable like this:

 let age = 38 

it is completely obvious that we can use string interpolation as follows:

 print("Hi, I'm \(age).") 

But what if we want to format the result somehow differently?

Using the new string interpolation system in in Swift 5.0 we can write an extension String.StringInterpolation to add our own interpolation method:

 extension String.StringInterpolation { mutating func appendInterpolation(_ value: Int) { let formatter = NumberFormatter() formatter.numberStyle = .spellOut if let result = formatter.string(from: value as NSNumber) { appendLiteral(result) } } } 

Now the code will output the integer variable as text: “Hi, I'm thirty-eight.”

We can use a similar technique to fix the date formatting, because the default date view in the form of a string is not very attractive:

 print("Today's date is \(Date()).") 

You will see that Swift will display the current date as something like: “2019-02-21 23:30:21 +0000”. We can make it more beautiful using our own date formatting:

 mutating func appendInterpolation(_ value: Date) { let formatter = DateFormatter() formatter.dateStyle = .full let dateString = formatter.string(from: value) appendLiteral(dateString) } 

Now the result looks much better, something like: “February 21, 2019 23:30:21”.

Note: to avoid possible confusion when working together in a team, you probably should not override the default Swift methods. Therefore, give the parameters names of your choice to avoid confusion:

 mutating func appendInterpolation(format value: Int) { 

Now we will call this method with the named parameter:

 print("Hi, I'm \(format: age).") 

Now it will be clear that we use our own implementation of the method.

Interpolation with parameters


This change shows that now we have complete control over how the interpolation of strings occurs.

For example, we can rewrite code to process Twitter messages:

 mutating func appendInterpolation(twitter: String) { appendLiteral("<a href=\"https://twitter.com/\(twitter)\">@\(twitter)</a>") } 

Now we can write this:

 print("You should follow me on Twitter: \(twitter: "twostraws").") 

But why should we limit ourselves to one parameter? For our example of formatting a number, there is no point in forcing users to use one conversion parameter (.spellOut) - so we change the method by adding a second parameter:

 mutating func appendInterpolation(format value: Int, using style: NumberFormatter.Style) { let formatter = NumberFormatter() formatter.numberStyle = style if let result = formatter.string(from: value as NSNumber) { appendLiteral(result) } } 

And use it like this:

 print("Hi, I'm \(format: age, using: .spellOut).") 

You can have as many parameters as you like, of any type. An example using @autoclosure for the default:

 extension String.StringInterpolation { mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) { if values.count == 0 { appendLiteral(defaultValue()) } else { appendLiteral(values.joined(separator: ", ")) } } } let names = ["Malcolm", "Jayne", "Kaylee"] print("Crew: \(names, empty: "No one").") 

Applying the @autoclosure attribute means that we can use simple values ​​for the default value or call complex functions. In the method, they will become a closure.

Now you may think that we can rewrite the code without using the interpolation functional, something like this:

 extension Array where Element == String { func formatted(empty defaultValue: @autoclosure () -> String) -> String { if count == 0 { return defaultValue() } else { return self.joined(separator: ", ") } } } print("Crew: \(names.formatted(empty: "No one")).") 

But now we have complicated the challenge - after all, we are obviously trying to format something, this is the point of interpolation. Remember the Swift rule - avoid unnecessary words.

Erica Sadun offered a really short and beautiful example of how to simplify the code:

 extension String.StringInterpolation { mutating func appendInterpolation(if condition: @autoclosure () -> Bool, _ literal: StringLiteralType) { guard condition() else { return } appendLiteral(literal) } } let doesSwiftRock = true print("Swift rocks: \(if: doesSwiftRock, "(*)")") print("Swift rocks \(doesSwiftRock ? "(*)" : "")") 

Add string interpolation for custom types


We can use string interpolation for our own types:

 struct Person { var type: String var action: String } extension String.StringInterpolation { mutating func appendInterpolation(_ person: Person) { appendLiteral("I'm a \(person.type) and I'm gonna \(person.action).") } } let hater = Person(type: "hater", action: "hate") print("Status check: \(hater)") 

Interpolation of lines is useful because we don’t touch on debug information about an object. If we look at it in debugger or display it, we will see intact data:

 print(hater) 

We can combine a custom type with several parameters:

 extension String.StringInterpolation { mutating func appendInterpolation(_ person: Person, count: Int) { let action = String(repeating: "\(person.action) ", count: count) appendLiteral("\n\(person.type.capitalized)s gonna \(action)") } } let player = Person(type: "player", action: "play") let heartBreaker = Person(type: "heart-breaker", action: "break") let faker = Person(type: "faker", action: "fake") print("Let's sing: \(player, count: 5) \(hater, count: 5) \(heartBreaker, count: 5) \(faker, count: 5)") 

Of course, you can use all the features of Swift to create your own formatting. For example, we can write an implementation that accepts any Encodable object and outputs it in JSON:

 mutating func appendInterpolation<T: Encodable>(debug value: T) { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted if let result = try? encoder.encode(value) { let str = String(decoding: result, as: UTF8.self) appendLiteral(str) } } 


If we make Person conforming to the Encodable protocol, then we can do this:

 print("Here's some data: \(debug: faker)") 

You can use features like a variable number of parameters or even mark your interpolation implementation as throwing . For example, our JSON formatting system does not react in any way in the event of a coding error, but we can fix this in order to analyze the error that occurred later:

 mutating func appendInterpolation<T: Encodable>(debug value: T) throws { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let result = try encoder.encode(value) let str = String(decoding: result, as: UTF8.self) appendLiteral(str) } print(try "Status check: \(debug: hater)") 

All that we have so far considered is just modifications of the methods for interpolating strings.

Creating your own types using interpolation


As you saw, it was about how to format the data in your application in a truly convenient way, but we can also create our own types using string interpolation.

To demonstrate this, we will create a new type, which is initialized from string using string interpolation.

 struct ColoredString: ExpressibleByStringInterpolation { //    -    -    struct StringInterpolation: StringInterpolationProtocol { //   -    var output = NSMutableAttributedString() //     var baseAttributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Georgia-Italic", size: 64) ?? .systemFont(ofSize: 64), .foregroundColor: UIColor.black] //   ,        init(literalCapacity: Int, interpolationCount: Int) { } // ,      mutating func appendLiteral(_ literal: String) { //   ,       print("Appending \(literal)") //    let attributedString = NSAttributedString(string: literal, attributes: baseAttributes) //     output.append(attributedString) } // ,          mutating func appendInterpolation(message: String, color: UIColor) { //     print("Appending \(message)") //        var coloredAttributes = baseAttributes coloredAttributes[.foregroundColor] = color //    -     let attributedString = NSAttributedString(string: message, attributes: coloredAttributes) output.append(attributedString) } } //    ,     let value: NSAttributedString //      init(stringLiteral value: String) { self.value = NSAttributedString(string: value) } //     init(stringInterpolation: StringInterpolation) { self.value = stringInterpolation.output } } let str: ColoredString = "\(message: "Red", color: .red), \(message: "White", color: .white), \(message: "Blue", color: .blue)" 

In fact, under the hood there is one syntactic sugar. We could write the final part manually:

 var interpolation = ColoredString.StringInterpolation(literalCapacity: 10, interpolationCount: 1) interpolation.appendLiteral("Hello") interpolation.appendInterpolation(message: "Hello", color: .red) interpolation.appendLiteral("Hello") let valentine = ColoredString(stringInterpolation: interpolation) 

Conclusion


As you saw, custom string interpolation allows us to place the formatting in one place, so that the method calls become simpler and clearer. It also provides us with excellent flexibility to create the required types in the most natural way possible.

Remember that this is only one of the possibilities - and not the only one. This means that sometimes we use interpolation, sometimes functions or something else. Like many things in development, you always need to choose the best way to solve a problem.

Source: https://habr.com/ru/post/447586/


All Articles