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.
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
}
}
}
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
** 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.
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 toStringInterpolationProtocol
can provide several differentappendInterpolation
methods with different behaviors. AnappendInterpolation
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].
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")
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.
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す!")
}
}
You can now easily decorate strings.
ExpressibleByStringInterpolation
.StringInterpolation
, which is an associated type of ExpressibleByStringInterpolation
, has a method called appendInterpolation
, which is responsible for variable expansion.String
uses DefaultStringInterpolation
Text ("hoge ")
, the type of " hoge "
becomes LocalizedStringKey
LocalizedStringKey
has its own StringInterpolation
LocalizedStringKey.StringInterpolation
implements appendInterpolation
which takes Text
and Image
as arguments, so these can be used as arguments.It was very refreshing to solve the mystery of the function that I used in Swift UI. After all you should read the document.
[^ 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