SwiftUI 中的 Text 插值和本地化 (下),喵神出品,必属精品

前言

学如逆水行舟,不进则退。共勉!!!

在上篇中我们已经看到为什么 Text,或者更准确地说,LocalizedStringKey,可以接受 ImageDate,而不能接受 Bool 或者自定义的 Person 类型了。在这下篇中,让我们具体看看有哪些方法能让 Text 支持其他类型。

为 LocalizedStringKey 自定义插值

如果我们只是想让 Text 可以直接接受 true 或者 false,我们可以简单地为加上 appendInterpolation 的 Bool 重载。

extension LocalizedStringKey.StringInterpolation {
    mutating func appendInterpolation(_ value: Bool) {
        appendLiteral(value.description)
    }
}

这样的话,我们就能避免编译错误了:

Text("3 == 3 is \(true)")

对于 Person,我们可以同样地添加 appendInterpolation,来直接为 LocalizedStringKey 增加 Person 版本的插值方法:

extension LocalizedStringKey.StringInterpolation {
    mutating func appendInterpolation(_ person: Person, isFriend: Bool) {
        appendLiteral(person.title(isFriend: isFriend))
    }
}

上面的代码为 LocalizedStringKey.StringInterpolation 添加了 Bool 和 Person 的支持,但是这样的做法其实破坏了本地化的支持。这可能并不是你想要的效果,甚至造成预料之外的行为。在完全理解前,请谨慎使用。在本文稍后关于本地化的部分,会对这个话题进行更多讨论。

LocalizedStringKey 的真面目

通过 key 查找本地化值

我们花了大量篇幅,一直都在 LocalizedStringKey 和它的插值里转悠。回头想一想,我们似乎还完全没有关注过 LocalizedStringKey 本身到底是什么。正如其名,LocalizedStringKey 是 SwiftUI 用来在 Localization.strings 中查找 key 的类型。试着打印一下最简单的 LocalizedStringKey 值:

let key1: LocalizedStringKey = "Hello World"
print(key1)
// LocalizedStringKey(
//     key: "Hello World", 
//     hasFormatting: false,
//     arguments: []
// )

它会查找 “Hello World” key 对应的字符串。比如在本地化字符串文件中有这样的定义:

// Localization.strings
"Hello World"="你好,世界";

那是使用时,SwiftUI 将根据 LocalizedStringKey.key 的值选取结果:

Text("Hello World")
Text("Hello World")
    .environment(\.locale, Locale(identifier: "zh-Hans"))
image.png
插值 LocalizedStringKey 的 key

那么有意思的部分来了,下面这个 LocalizedStringKey 的 key 会是什么呢?

let name = "onevcat"
let key2: LocalizedStringKey = "I am \(name)"

是 "I am onevcat" 吗?如果是的话,那这个字符串要如何本地化?如果不是的话,那 key 会是什么?

打印一下看看就知道了:

print(key2)

// LocalizedStringKey(
//     key: "I am %@", 
//     hasFormatting: true, 
//     arguments: [
//         SwiftUI.LocalizedStringKey.FormatArgument(
//             ...storage: Storage.value("onevcat", nil)
//         )
//     ]
// )

key 并不是固定的 “I am onevcat”,而是一个 String formatter:”I am %@”。熟悉 String format 的读者肯定对此不会陌生:name 被作为变量,会被传递到 String format 中,并替换掉 %@ 这个表示对象的占位符。所以,在本地化这个字符串的时候,我们需要指定的 key 是 “I am %@”。当然,这个 LocalizedStringKey 也可以对应其他任意的输入:

// Localization.strings
"I am %@"="我是%@";

// ContentView.swift
Text("I am \("onevcat")")
// 我是onevcat

Text("I am \("张三")")
// 我是张三

对于 Image 插值来说,情况很相似:Image 插值的部分会被转换为 %@,以满足本地化 key 的需求:

let key3: LocalizedStringKey = "Hello \(Image(systemName: "globe"))"

print(key3)
// LocalizedStringKey(
//     key: "Hello %@", 
//     ...
// )

// Localization.strings
// "Hello %@"="你好,%@";

Text("Hello \(Image(systemName: "globe"))")
Text("Hello \(Image(systemName: "globe"))")
    .environment(\.locale, Locale(identifier: "zh-Hans"))
image.png

值得注意的一点是,Image 的插值对应的格式化符号是 %@,这和 String 的插值或者其他一切对象插值所对应的符号是一致的。也就是说,下面的两种插值方式所找到的本地化字符串是相同的:

Text("Hello \("onevcat")")
    .environment(\.locale, Locale(identifier: "zh-Hans"))
Text("Hello \(Image(systemName: "globe"))")
    .environment(\.locale, Locale(identifier: "zh-Hans"))
image.png
其他类型的插值格式化

可能你已经猜到了,除了 %@ 外,LocalizedStringKey 还支持其他类型的格式化,比如在插值 Int 时,会把 key 中的参数转换为 %lld;对 Double 则转换为 %lf 等:

let key4: LocalizedStringKey = "Hello \(1))"
// LocalizedStringKey(key: "Hello %lld)

let key5: LocalizedStringKey = "Hello \(1.0))"
// LocalizedStringKey(key: "Hello %lf)

使用 Hello %lld 或者 Hello %lf,是不能在本地化文件中匹配到之前的 Hello %@ 的。

更合理的 appendInterpolation 实现

避免 appendLiteral

现在让我们回到 Bool 和 Person 的插值这个话题。在本篇一开始,我们添加了两个插值方法,来让 LocalizedStringKey 接受 Bool 和 Person 的插值:

mutating func appendInterpolation(_ value: Bool) {
    appendLiteral(value.description)
}

mutating func appendInterpolation(_ person: Person, isFriend: Bool) {
    appendLiteral(person.title(isFriend: isFriend))
}

在两个方法中,我们都使用了 appendLiteral 来将 String 直接添加到 key 里,这样做我们得到的会是一个完整的,不含参数的 LocalizedStringKey,在大多数情况下,这不会是我们想要的结果:

let key6: LocalizedStringKey = "3 == 3 is \(true)"
// LocalizedStringKey(key: "3 == 3 is true", ...)

let person = Person(name: "Geralt", place: "Rivia", nickName: "White Wolf")
let key7: LocalizedStringKey = "Hi, \(person, isFriend: false)"
// LocalizedStringKey(key: "Hi, Geralt of Rivia", ...)

在实现新的 appendInterpolation 时,尊重插入的参数,将实际的插入动作转发给已有的 appendInterpolation 实现,让 LocalizedStringKey 类型去处理 key 的合成及格式化字符,应该是更合理和具有一般性的做法:

mutating func appendInterpolation(_ value: Bool) {
    appendInterpolation(value.description)
}

mutating func appendInterpolation(_ person: Person, isFriend: Bool) {
    appendInterpolation(person.title(isFriend: isFriend))
}

let key6: LocalizedStringKey = "3 == 3 is \(true)"
// LocalizedStringKey(key: "3 == 3 is %@", ...)

let key7: LocalizedStringKey = "Hi, \(person, isFriend: false)"
// LocalizedStringKey(key: "Hi, %@", ...)
为 Text 添加样式

结合利用 LocalizedStringKey 参数插值和已有的 appendInterpolation,可以写出一些简便方法。比如可以添加一组字符串格式化的方法,来让 Text 的样式设置更简单一些:

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

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

    mutating func appendInterpolation(italic value: LocalizedStringKey) {
        appendInterpolation(Text(value).italic())
    }
    
    mutating func appendInterpolation(_ value: LocalizedStringKey, color: Color?) {
        appendInterpolation(Text(value).foregroundColor(color))
    }
}
Text("A \(bold: "wonderful") serenity \(italic: "has taken") \("possession", color: .red) of my \(underline: "entire soul").")

可以得到如下的效果:

image.png

对应的 key 是 “A %@ serenity %@ %@ of my %@.”。插值的地方都会被认为是需要参数的占位符。在一些情况下可能这不是你想要的结果,不过 attributed string 的本地化在 UIKit 中也是很恼人的存在。相对于 UIKit 来说,SwiftUI 在这方面的进步还是显而易见的。

关于 _FormatSpecifiable

最后我们来看看关于 _FormatSpecifiable 的问题。可能你已经注意到了,在内建的 LocalizedStringKey.StringInterpolation 有两个方法涉及到了 _FormatSpecifiable:

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

指定占位格式

Swift 中的部分基本类型,是满足 _FormatSpecifiable 这个私有协议的。该协议帮助 LocalizedStringKey 在拼接 key 时选取合适的占位符表示,比如对 Int 选取 %lld,对 Double 选取 %lf 等。当我们使用 Int 或 Double 做插值时,上面的重载方法将被使用:

Text("1.5 + 1.5 = \(1.5 + 1.5)")

// let key: LocalizedStringKey = "1.5 + 1.5 = \(1.5 + 1.5)"
// print(key)
// 1.5 + 1.5 = %lf

上面的 Text 等号右边将按照 %lf 渲染:


image.png

如果只想要保留到小数点后一位,可以直接用带有 specifier 参数的版本。在生成 key 时,会用传入的 specifier 取代原本应该使用的格式:

Text("1.5 + 1.5 = \(1.5 + 1.5, specifier: "%.1lf")")

// key: 1.5 + 1.5 = %.1lf
image.png
为自定义类型实_FormatSpecifiable

虽然是私有协议,但是 _FormatSpecifiable 相对还是比较简单的:

protocol _FormatSpecifiable: Equatable {
    associatedtype _Arg
    var _arg: _Arg { get }
    var _specifier: String { get }
}

让 _arg 返回需要被插值的实际值,让 _specifier 返回占位符的格式,就可以了。比如可以猜测 Int: _FormatSpecifiable 的实现是:

extension Int: _FormatSpecifiable {
    var _arg: Int { self }
    var _specifier: String { "%lld" }
}

对于我们在例子中多次用到的 Person,也可以用类似地手法让它满足 _FormatSpecifiable:

extension Person: _FormatSpecifiable {
    var _arg: String { "\(name) of \(place)" }
    var _specifier: String { "%@" }
}

这样一来,即使我们不去为 LocalizedStringKey 添加 Person 插值的方法,编译器也会为我们选择 _FormatSpecifiable 的插值方式,将 Person 的描述添加到最终的 key 中了。

总结

上篇的基础上,在本文中:

  • 我们尝试扩展了 LocalizedStringKey 插值的方法,让它支持了 BoolPerson
  • LocalizedStringKey 插值的主要任务是自动生成合适的,带有参数的本地化 key。
  • 在扩展 LocalizedStringKey 插值时,应该是尽可能使用 appendInterpolation,避免参数“被吞”。
  • 插值的格式是由 _FormatSpecifiable 确定的。我们也可以通过让自定义类型实现这个协议的方式,来进行插值。

至此,为什么 Text 中可以插值 Image,以及它背后发生的所有事情,我们应该都弄清楚了。

推荐阅读:SwiftUI 中的 Text 插值和本地化 (上)

更多ios资料请关注简书个人主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容