[Swift] I investigated the variable expansion "\ ()" that realizes the mysterious function of Swift UI.

This article is the 20th day article of Swift Part 2 Advent Calendar 2020. Let's look at variable expansion [^ 1] in Swift, focusing on the example of Swift UI.

Mysterious features of Swift UI

You can write the following code in SwiftUI.

struct MyView: View {
    var body: some View {
        HStack{
            Text("this is\(Image(systemName: "swift"))is")  //OK
            Text("this is\(Text("Bold letters").bold())is")      //OK
        }
    }
}

image.png

It's insanely convenient. Until recently I thought it was magic. By the way, the following code gives an error.

    var body: some View {
        HStack{
            Text("this is\(Button("button"){})is") //Instance method 'appendInterpolation' requires that 'Button<Text>' conform to '_FormatSpecifiable'
            Text("this is\(Color.red)is")         //Instance method 'appendInterpolation' requires that 'Color' conform to '_FormatSpecifiable'
        }
    }

It's understandable that Buttons can't be embedded in text. However, the following code is also an error

    var body: some View {
        HStack{
            Text("this is\(false)is") //No exact matches in call to instance method 'appendInterpolation'
        }
    }

Normally this Text should display" This is false ". However, that is not the case.

Also, if you do this, you will not be able to display the image.

struct MyView: View {
    let text: String
    var body: some View {
        HStack{
            Text(text)
        }
    }
}

MyView(text: "this is\(Image(systemName: "swift"))is")   //this isImage(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.(unknown context at $1905e5fc0).NamedImageProvider>)is

image.png

** Something is wrong **.

Reading the error, it seems that appendInterpolation and _FormatSpecifiable are important. To solve this mystery, let's first look at appendInterpolation.

ExpressibleByStringInterpolation Swift has a protocol called ExpressibleByStringLiteral that allows types to be initialized with string literals.

For example, if you create the following structure, you can initialize it with a string literal.

struct MyString: ExpressibleByStringLiteral, CustomStringConvertible {
    typealias StringLiteralType = String

    let body: String

    init(stringLiteral: Self.StringLiteralType){
        self.body = stringLiteral
    }

    var description: String {
        return body
    }
}

let myString: MyString = "a-I-U-E-O"       //OK

However, this alone causes this code to be an error.

let myString: MyString = "\("a-I-U-E-O")"  //error

This is because ExpressibleByStringLiteral is not enough for variable expansion. That's where ExpressibleByStringInterpolation comes in.

extension MyString: ExpressibleByStringInterpolation {}
let myString: MyString = "\("a-I-U-E-O")"  //OK

Now you can safely use variable expansion [^ 2].

This ExpressibleByStringInterpolation requires a type called StringInterpolation that conforms to the StringInterpolationProtocol as an associated type. By default this is specified as DefaultStringInterpolation.

associatedtype StringInterpolation : StringInterpolationProtocol = DefaultStringInterpolation where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType

In other words, by adding this protocol, StringInterpolation and some methods were added to MyString, and variable expansion became possible by their function.

Types that conform to the StringInterpolationProtocol have methods appendInterpolation and appendLiteral. Yes, the appendInterpolation mentioned earlier seems to be the method called when expanding variables.

Expand variable expansion

To understand appendInterpolation, let's create a new variable expansion by ourselves.

According to the Documentation (https://developer.apple.com/documentation/swift/stringinterpolationprotocol), you can customize variable expansion by defining an additional appendInterpolation.

appendInterpolation methods support virtually all features of methods: they can have any number of parameters, can specify labels for any or all of their parameters, can provide default values, can have variadic parameters, and can have parameters with generic types. Most importantly, they can be overloaded, so a type that conforms to StringInterpolationProtocol can provide several different appendInterpolation methods with different behaviors. An appendInterpolation method can also throw; when a user writes a literal with one of these interpolations, they must mark the string literal with try or one of its variants.

The StringInterPolation of String is the default DefaultStringInterpolation, so you can extend it to extend the normal String variable expansion. let's do it.

extension DefaultStringInterpolation {
    mutating func appendInterpolation(reversed value: String) {
        self.appendInterpolation(String(value.reversed()))
    }
}

let string = "a-I-U-E-O"
print("Reversed string: \(reversed: string)")  //Oevia

With this alone, a new notation of " \ (reversed: string) " has been defined [^ 3].

Variable expansion in Swift UI

Now, let's see how Swift UI works. According to [Documentation](https://developer.apple.com/documentation/swiftui/text/init(_:) -9d1g4), Text works as follows.

SwiftUI doesn’t call the init(_:) method when you initialize a text view with a string literal as the input. Instead, a string literal triggers the init(_:tableName:bundle:comment:) method — which treats the input as a LocalizedStringKey instance — and attempts to perform localization.

If you call the initializer without specifying it, it seems to call init (_: tableName: bundle: comment :), but the first argument of this initializer is of type LocalizedStringKey.

This LocalizedStringKey is a type that conforms to ExpressibleByStringInterpolation, and there is a struct called StringInterpolation inside. In other words, LocalizedStringKey uses the independently implemented StringInterpolation instead of DefaultStringInterpolation.

This unique StringInterpolation has the following methods:

func appendInterpolation<T>(T)
func appendInterpolation(String)
func appendInterpolation(Text)
func appendInterpolation(Image)
func appendInterpolation(ClosedRange<Date>)
func appendInterpolation(DateInterval)
func appendInterpolation<Subject>(Subject, formatter: Formatter?)
func appendInterpolation<Subject>(Subject, formatter: Formatter?)
func appendInterpolation<T>(T, specifier: String)
func appendInterpolation(Date, style: Text.DateStyle)

Now you can explain the mysterious behavior mentioned at the beginning. The reason why Text and Image can be interpolated in Text is that the following two methods are provided respectively.

func appendInterpolation(Text)
func appendInterpolation(Image)

Also, the top appendInterpolation <T> (T) is defined as:

mutating func appendInterpolation<T>(_ value: T) where T : _FormatSpecifiable

In other words, when I tried to put Button or Color in the argument of this method, I was angry that it was not compliant with _FormatSpecifiable.

If you receive an argument of Text as String, the image cannot be displayed because you should specify it in the first place, not String but LocalizedStringKey. Therefore, the solution is as follows.

struct MyView: View {
    let text: LocalizedStringKey
    var body: some View {
        HStack{
            Text(text)
        }
    }
}

MyView(text: "this is\(Image(systemName: "swift"))is")

image.png

I often see custom view implementations receiving the value used as an argument to Text as String, but it may be better to receive it as LocalizedStringKey.

However, there are some situations where LocalizedStringKey is troublesome, such as "\ (false)" becoming an error. For this reason, Text has the following initializers. This interprets the arguments as verbatim (literally).

init(verbatim content: String)

If you use this, you can solve the problem that Bool cannot be inserted as follows.

    var body: some View {
        HStack{
            Text(verbatim: "this is\(false)is")
        }
    }

of course

    var body: some View {
        HStack{
            Text("this is\(false)is" as String)
        }
    }

But I think it's okay.

Extend Text variable expansion

I've researched it, so let's expand it at the end. It's easy because you just write the extension.

extension LocalizedStringKey.StringInterpolation{
    mutating func appendInterpolation(bold value: LocalizedStringKey){
        self.appendInterpolation(Text(value).bold())
    }

    mutating func appendInterpolation(underline value: LocalizedStringKey){
        self.appendInterpolation(Text(value).underline())
    }

    mutating func appendInterpolation(italic value: LocalizedStringKey){
        self.appendInterpolation(Text(value).italic())
    }

    mutating func appendInterpolation(systemImage name: String){
        self.appendInterpolation(Image(systemName: name))
    }
}

If you define it like this

struct MyView: View {
    var body: some View {
        Text("this is\(bold: "Bold")so\(underline: "Underline")so\(italic: "italic")so\(systemImage: "swift")soす!")
    }
}

image.png You can now easily decorate strings.

Conclusion

It was very refreshing to solve the mystery of the function that I used in Swift UI. After all you should read the document.

Reference material

[^ 1]: String Interpolation seems to be translated as string interpolation or string insertion, but for convenience, we will call it variable expansion. [^ 2]: Note that ExpressibleByStringInterpolation conforms to ExpressibleByStringLiteral, so you can use this from the beginning. [^ 3]: StringInterPolationProtocol requires an implementation of appendLiteral, but according to the documentation, Swift calls appendInterpolation for variable expansion. There may be something wrong with this area.

Recommended Posts

[Swift] I investigated the variable expansion "\ ()" that realizes the mysterious function of Swift UI.
[Swift] I tried to implement the function of the vending machine
I investigated the mechanism of attr_accessor (* Hoge :: ATTRIBUTES) that I sometimes see
I investigated the internal processing of Retrofit
[Swift] Termination of the program by the fatalError function
List of devices that can be previewed in Swift UI
Now, I understand the coordinate transformation method of UIView (Swift)
[Swift] Lightly introduce the logic of the app that passed the selection
[Swift] I tried using ColorPicker (the one that can easily select colors) added in iOS14 [Swift UI]
I investigated the enclosing instance.
I will explain the nesting of for statements that kill beginners
The story of Collectors.groupingBy that I want to keep for posterity
[Swift] How to implement the Twitter login function using Firebase UI ①
I tried using the cache function of Application Container Cloud Service
[Swift UI] How to get the startup status of the application [iOS]
[Swift] Variable shapes that were not taught in the introductory book
[Swift] How to implement the Twitter login function using Firebase UI ②