SwiftUI 应用内切换 app 语言
- Published on
- Authors
- Name
- zzzwco
- @zzzwco
Xcode 14.3
iOS 16.4
macOS 13.3
示例效果:
保存和更新语言选项
App 语言的国际化是由系统设置的语言决定的,SwiftUI 中凡是用到字符串的地方,基本都支持 LocalizedStringKey,我们无需额外设置,就能很好地实现国际化。
如果要实现应用内语言切换,首先需要保存用户当前选择的语言。使用 @AppStorage
是一个不错的选择,它不仅能够保存用户的偏好设置,又能够驱动视图更新。
final class AppState: ObservableObject {
@AppStorage("language") var language = "en"
}
AppState
用于存储 app 的全局状态,这里我们只保存了用户选择的语言。在 app 的根视图注入它就能在任意视图上读写 app 的状态,这非常方便:
@main
struct swiftui_change_languageApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
rootView
.environmentObject(appState)
}
}
private var rootView: some View {
TabView {
// ...
}
}
}
在切换语言的视图中,通过 AppState 获取当前的语言设置的语言并高亮该选项,在选择不同的语言是,更新 appState 中的 language
变量时,会同时保存该值。
struct ChangeLanguageView: View {
@EnvironmentObject private var appState: AppState
private let languages = [
"English": "en",
"中文": "zh-Hans"
]
var body: some View {
Form {
ForEach(Array(languages.keys), id: \.self) { v in
LabeledContent(v) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
.opacity(isSelected(v) ? 1 : 0)
}
.contentShape(Rectangle())
.onTapGesture {
if isSelected(v) { return }
appState.language = languages[v]!
}
}
}
.navigationTitle("Language".localized)
.navigationBarTitleDisplayMode(.inline)
}
private func isSelected(_ language: String) -> Bool {
appState.language == languages[language]
}
}
如上代码中的 "en" 和 "zh-Hans" 对应的是国际化语言文件夹的名称前缀,后面会用到。
关于语言文件名称的查看,可以选中工程中的 Localizable.strings 文件右键 Show in Finder 查看,也可以直接选中相应的文件,在 Xcode 中查看。
更新本地化内容
我们已经在 appState 中保存了当前选择的语言("en" 或 "zh-Hans"),接下来就是找到对应的文件并获取相应的文本内容。为了便于调用,我们可以为 String 添加一个扩展属性:
extension String {
var localized: String {
let res = UserDefaults.standard.string(forKey: "language")
let path = Bundle.main.path(forResource: res, ofType: "lproj")
let bundle: Bundle
if let path = path {
bundle = Bundle(path: path) ?? .main
} else {
bundle = .main
}
return NSLocalizedString(self, bundle: bundle, value: "", comment: "")
}
}
上面的 ChangeLanguageView
文件中已经展示了其用法:"Language".localized
。
当切换语言时,ChangeLanguageView 的导航栏确实立即更新了,可是两个 tab 视图并没有更新。因为 .localized
扩展并不具备驱动视图更新的能力。
一种很容易想到的思路是封装一个 LocalizedText 视图,获取环境变量 appState 并更新相应的文本内容。但使用起来较为繁琐,而且 SwiftUI 中有些视图只支持 String 类型的参数,LocalizedText 无能为力。
还有一种更简单的办法,那就是在 app 中已经存在于视图树中却无法监听到语言改变的视图内作如下声明:
struct HomeView: View {
// AppState holds global application settings and triggers view refreshes upon changes.
@EnvironmentObject private var appState: AppState
var body: some View {
VStack {
Text("Home".localized)
}
}
}
虽然我们并没有使用 @EnvironmentObject
属性 appState,但 HomeView
可以监听到它的状态改变从而更新视图。
其它
虽然 SwiftUI 有着高效的 diffing 算法,视图的频繁刷新一般不会产生很大的性能开销。如果确实需要优化性能,建议将上面的 AppState
中关于语言偏好设置的属性拆分到另一个类中,减少不必要的视图更新。
此外,虽然通过声明 @EnvironmentObject
属性 appState 能够解决问题,但仍然稍显繁琐,笔者期待有更好的方案实现应用内语言切换。