WWDC Quick Look 💓 By SwiftGGTeam
Get it right (to left)

Get it right (to left)

观看原视频

Highlight

阿拉伯语是 Apple 平台使用量前十的语言之一。RTL(Right-to-Left)语言不只是文字方向反转——它影响整个 UI 布局,从工具栏到侧边栏,从图标到数字显示。


核心内容

你可能已经把应用本地化到英语、法语、日语和中文。接下来要支持阿拉伯语或希伯来语时,问题会变得不同:文字从右向左排,段落里还可能夹着英文产品名和数字。

00:22)Rich Gillam 在开场指出,阿拉伯语是 Apple 平台上使用量前十的语言之一。Apple 平台还为 15 种从右到左语言提供字体和键盘支持。

这个方向变化会传导到整个界面。(02:17)在 Hebrew 版 Numbers 帮助页面里,文字靠右对齐,表格和图片位置反转;放到 Safari 窗口后,工具栏也从右向左排列;再放到整个系统,菜单栏、Dock、侧边栏、标签栏都跟着翻转。

好消息是,系统已经承担了大部分工作。(03:21)CoreText 会处理双向文本,Apple 的 UI 框架会默认设置自然书写方向和自然对齐,标准控件会自动翻转。开发者要处理的是边界情况:哪些图标该镜像,哪些控件该保持绝对方向,哪些数字要按用户地区和偏好显示。


详细内容

文本先交给系统处理

04:21)右到左语言经常出现双向文本。一个希伯来语句子里可以包含 Pages、Numbers、Keynote 这样的英文词,英文仍然从左向右读,整个段落的片段顺序从右向左。

05:18)大多数文本控件不需要手动设置方向。CoreText 会安排同一行里不同方向的字符,UIKit、AppKit 和 SwiftUI 的文本控件会使用 natural writing direction 和 natural alignment。阿拉伯语或希伯来语 UI 中,文本控件默认从右向左写,并靠右对齐。

这条规则的实用含义很简单:普通正文、按钮标题、表单输入框,先保持系统默认。只有在表达绝对方向时,才考虑覆盖默认行为。

图标要区分“方向”和“语义”

07:50)自定义图片的 RTL 行为在 Xcode imageset editor 里设置。图片不随语言变化时选择 Fixed;可算法镜像时选择 Mirrors;无法简单镜像时选择 Both,为 LTR 和 RTL 分别提供图片。

09:06)如果使用 SF Symbols,很多本地化已经由系统处理。需要随阅读方向变化的 symbol 会自动翻转;问号这类符号也有面向阿拉伯语的本地化版本。

10:17)箭头要看名字。arrow.backward.circle 表示“返回”,在 RTL 里会翻转;arrow.left.circle 表示绝对左侧,在 RTL 里不会翻转。

Label("Preview", systemImage: "arrowtriangle.forward.fill")
Label("Left", systemImage: "arrow.left")
Label("Right", systemImage: "arrow.right")

关键点:

  • arrowtriangle.forward.fill 使用 forward,表达阅读方向里的“前进”,会随 RTL 翻转。
  • arrow.left 使用绝对方向 left,始终指向左。
  • arrow.right 使用绝对方向 right,始终指向右。

SwiftUI 里用环境控制控件方向

12:55)标准控件会自动随 UI 方向翻转。按钮带文字和图标时,SwiftUI 通过 LabelStyle 控制图标在文字哪一侧,必要时再用环境值固定某一块视图的布局方向。

struct ContentView: View {
    var body: some View {
    VStack(alignment: .leading) {
        Button(action: {}) {
            Label("Preview", systemImage: "arrowtriangle.forward.fill")
        }.labelStyle(IconOnRightLabelStyle())
            
            HStack() {
                Button(action: {}) {
                    Label("Left", systemImage: "arrow.left")
                }.labelStyle(TitleAndIconLabelStyle())

                Button(action: {}) {
                    Label("Right", systemImage: "arrow.right")
                }.labelStyle(IconOnRightLabelStyle())
            }.environment(\.layoutDirection, .leftToRight)
        }.padding()
    }
}

关键点:

  • VStack(alignment: .leading) 使用 leading,不写死左侧。
  • arrowtriangle.forward.fill 用于 Preview,表达相对阅读方向的前进。
  • TitleAndIconLabelStyle() 把 Left 按钮的图标放在阅读方向的标题前方。
  • IconOnRightLabelStyle() 是自定义样式,让 Preview 和 Right 的图标在标题后方。
  • .environment(\.layoutDirection, .leftToRight) 只加在包含 Left 和 Right 的 HStack 上,因此这两个按钮保持从左到右排列。
  • Preview 没有继承这个环境覆盖,所以它仍然会在 RTL UI 中按系统规则反转。

14:22)自定义 label style 的实现很小。它只需要把标题和图标按固定顺序放入 HStack

struct IconOnRightLabelStyle : LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack {
            configuration.title
            configuration.icon
        }
    }
}

关键点:

  • IconOnRightLabelStyle 实现 LabelStyle
  • makeBody(configuration:) 接收系统提供的 title 和 icon。
  • HStack 的子视图顺序决定显示顺序。
  • configuration.title 先放入,标题显示在前。
  • configuration.icon 后放入,图标显示在后。

16:32)AppKit 和 UIKit 有相同的概念。Interface Builder 里用 Leading、Trailing、Left、Right;AppKit 代码里是 button 的 imagePosition;UIKit 里是 button configuration 的 imagePlacement。Leading 和 Trailing 随 UI 方向变化,Left 和 Right 保持绝对方向。

分段控件和布局要按用途选择是否翻转

18:58)文字样式按钮 Bold、Italic、Underline 这种分段控件可以随 UI 方向翻转;文本对齐按钮表示绝对左、中、右,应该保持顺序不变。

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Picker(selection: $textStyle, label: Text("Text Style")) {
                Text("B").tag(TextStyle.bold)
                Text("I").tag(TextStyle.italic)
                Text("U").tag(TextStyle.underline)
                Text("S").tag(TextStyle.strikethrough)
            }.pickerStyle(.segmented)

            Picker(selection: $alignment, label: Text("Alignment")) {
                Image(systemName: "text.alignleft").tag(TextAlignment.left)
                Image(systemName: "text.aligncenter").tag(TextAlignment.center)
                Image(systemName: "text.alignright").tag(TextAlignment.right)
           }.pickerStyle(.segmented)
             .environment(\.layoutDirection, .leftToRight)
        }
    }
}

关键点:

  • 第一个 Picker 表示文字样式,保持默认 RTL 行为。
  • 第二个 Picker 表示文本对齐,图标有绝对方向含义。
  • text.alignlefttext.aligncentertext.alignright 对应固定的左、中、右。
  • .environment(\.layoutDirection, .leftToRight) 只作用于对齐控件,让它在 RTL UI 中也按左、中、右显示。

19:16)UIKit 用 semanticContentAttribute 表达控件语义。默认 Unspecified 会随 UI 方向翻转;Spatial 表示空间方向控件,适合对齐控制;也可以强制 left-to-right 或 right-to-left。AppKit 里用 NSControl 的 Layout 和 Mirror 设置,或在代码里设置 userInterfaceLayoutDirection

多行标签要处理文本框内部对齐

22:38)表单里的单行标签只要对齐文本对象本身,多行标签还要对齐文本对象内部的每一行。SwiftUI 用 multilineTextAlignment 处理这个问题。

var body: some View {
   Form {
        TextField("Password:", text: $password)
        TextField("Verify:", text: $verifyPassword)
        TextField("Password Hint:\n(Recommended)", text: $passwordHint)
            .multilineTextAlignment(.trailing)
    }.padding()
}

关键点:

  • Form 负责表单标签和输入框的整体布局。
  • 前两个 TextField 是单行标签,系统能直接对齐它们的边界框。
  • 第三个 TextField 的标签包含换行,需要处理文本内部每一行。
  • .multilineTextAlignment(.trailing) 使用 trailing,对齐到阅读方向的尾侧。
  • 在 RTL UI 中,trailing 的物理方向会反转。

Auto Layout 优先使用 leading 和 trailing

26:32)标准视图、导航控制器、页面控制器、表格和集合视图都会按 UI 方向自动调整布局和滚动方向。手写布局时,也要让约束表达“阅读方向”,少用固定的左和右。

myView.leadingAnchor.constraint(equalTo: mySuperView.leadingAnchor, constant:16)

关键点:

  • leadingAnchor 表示阅读开始的一侧。
  • 在 LTR UI 中,leading 是左侧。
  • 在 RTL UI 中,leading 是右侧。
  • constant:16 保持边距,方向由 Auto Layout 根据 UI 语言处理。

26:53)如果确实要固定绝对左和右,Interface Builder 的约束菜单里可以关闭 Respect language direction,让 leading/trailing 变成 left/right。但 Session 建议把这种情况视为少数例外。

数字要通过本地化和格式化生成

28:02)阿拉伯语和印地语都可能使用非 Latin 数字。阿拉伯语是否使用 Arabic-Indic digits 还取决于国家和用户偏好。把数字转成字符串再拼接,会固定成 Latin digits。

myLabel.string = String(localized: "There are \(peopleInChat) people in this chat.",
                        comment: "Label indicating number of chat participants")

Text("There are \(peopleInChat) people in this chat.",
     comment: "Label indicating number of chat participants")

关键点:

  • String(localized:) 会从应用 bundle 中查找可翻译字符串。
  • \(peopleInChat) 的插值值会按用户 locale 和偏好渲染数字。
  • comment 给翻译者解释上下文。
  • SwiftUI 的 Text 初始化器也会正确处理插值里的数字。

30:12)即使数字在编译期已知,也建议运行时插值,让同一份阿拉伯语本地化能跟随用户数字偏好。

myLabel.string = String(localized: "This application supports \(3) file formats.",
                        comment: "Label showing number of supported file formats
                        (number is always 3)")

关键点:

  • \(3) 把固定数字也交给本地化格式化流程。
  • 只保留一份阿拉伯语或印地语翻译,不需要按数字系统复制本地化文件。
  • 注释明确说明这个数字恒为 3,避免翻译者误解业务含义。

31:41)百分号、货币符号和单位的位置也不能手写拼接。阿拉伯语、希伯来语、土耳其语里的百分号位置可能不同,Arabic-Indic digits 还会使用不同的百分号字符。

myLabel.stringValue = String(localized: "\(percentComplete.formatted(.percent)) complete")

关键点:

  • percentComplete.formatted(.percent) 让格式化器决定百分号、数字和符号位置。
  • String(localized:) 把格式化结果插入到本地化句子里。
  • 运行时插值会加入方向标记,避免数字和周围 RTL 文本互相干扰。

用伪语言测试 RTL

32:21)测试 RTL 不需要先完成阿拉伯语或希伯来语翻译。在 Xcode Scheme Editor 的 Options 里,把 App Language 设为 Right-to-Left Pseudolanguage。应用仍显示开发语言,但界面会按 RTL 翻转。

这一步适合放进常规测试流程。它能快速暴露写死 left/right、错误镜像图标、控件顺序错误、数字格式拼接等问题。


核心启发

1. 给现有表单做 RTL 体检

  • 做什么:把登录、支付、资料编辑等表单跑在 Right-to-Left Pseudolanguage 下,检查标签、输入框、多行提示和按钮顺序。
  • 为什么值得做:Session 明确指出标准控件会自动翻转,但多行标签和绝对方向控件仍需要人工判断。
  • 怎么开始:先在 Xcode Scheme Editor 里选择 RTL 伪语言,再把多行标签加上 .multilineTextAlignment(.trailing),把必须固定方向的控件加上 layoutDirection 覆盖。

2. 审计所有方向性 SF Symbols

  • 做什么:列出应用里的箭头、播放、返回、展开、缩进等 symbol,判断它们表达的是阅读方向还是绝对方向。
  • 为什么值得做forward/backward 会随 RTL 翻转,left/right 保持固定,选错名字会让导航或空间操作含义出错。
  • 怎么开始:把“下一步/返回”改用 forwardbackward 系列,把“向左对齐/向右移动”保留 leftright 系列。

3. 修掉用户可见字符串里的数字拼接

  • 做什么:搜索 + 拼接、String(format:)、手写 % 或货币符号的 UI 字符串。
  • 为什么值得做:阿拉伯语、印地语和用户数字偏好会改变数字字符;百分号和货币符号的位置也随语言变化。
  • 怎么开始:把用户可见字符串迁移到 String(localized:) 或 SwiftUI Text 插值,把百分比改成 value.formatted(.percent)

4. 给自定义图片补齐方向设置

  • 做什么:检查 Asset Catalog 中带方向含义的自定义图片,决定 Fixed、Mirrors 或 Both。
  • 为什么值得做:SF Symbols 能自动处理很多本地化图标,自定义图片仍需要在 imageset editor 中明确方向行为。
  • 怎么开始:对可简单镜像的图片选择 Mirrors;对包含文字、阴影或多个方向元素的图片选择 Both 并提供 LTR/RTL 两份资源。

关联 Session

评论

GitHub Issues · utterances