无需重启App实现UIKit等内部框架本地化的技术求助
解决UIKit框架无需重启即时本地化的问题
我太懂这个痛点了——自定义内容能秒切语言,但UIKit自带的控件(比如UIAlertController、UINavigationBar返回按钮、UIBarButtonItem这些)必须重启App才能更新,体验真的拉胯。你的现有方案已经搞定了自定义内容的本地化拦截,但漏掉了UIKit内部Bundle的字符串读取逻辑,下面给你一套完整的无需重启就能全量生效的方案:
1. 升级Bundle拦截逻辑(覆盖UIKit系统Bundle)
你的AnyLanguageBundle只替换了Bundle.main的类,但UIKit的系统控件会从Bundle(for: UIKit.self)这类系统Bundle读取字符串,所以我们需要用**方法交换(Method Swizzling)**来全局拦截所有Bundle的localizedString调用:
修改BundleExtension.swift为以下内容:
import UIKit var bundleKey: UInt8 = 0 private var currentLanguage: String? class AnyLanguageBundle: Bundle { override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String { guard let path = objc_getAssociatedObject(self, &bundleKey) as? String, let bundle = Bundle(path: path) else { return super.localizedString(forKey: key, value: value, table: tableName) } return bundle.localizedString(forKey: key, value: value, table: tableName) } } extension Bundle { static func setLanguage(_ language: String) { currentLanguage = language // 保存到UserDefaults,确保下次启动生效 UserDefaults.standard.set([language], forKey: "AppleLanguages") UserDefaults.standard.synchronize() // 替换Bundle.main的类 object_setClass(Bundle.main, AnyLanguageBundle.self) // 给Bundle.main关联当前语言的lproj路径 let languagePath = Bundle.main.path(forResource: language, ofType: "lproj") ?? "" objc_setAssociatedObject(Bundle.main, &bundleKey, languagePath, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // 交换所有Bundle的localizedString方法,覆盖系统Bundle swizzleLocalizedString() } private static func swizzleLocalizedString() { let originalSelector = #selector(localizedString(forKey:value:table:)) let swizzledSelector = #selector(swizzled_localizedString(forKey:value:table:)) guard let originalMethod = class_getInstanceMethod(Bundle.self, originalSelector), let swizzledMethod = class_getInstanceMethod(Bundle.self, swizzledSelector) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) } @objc private func swizzled_localizedString(forKey key: String, value: String?, table tableName: String?) -> String { // 如果是当前App的Bundle,用我们的自定义逻辑 if self == Bundle.main { guard let path = objc_getAssociatedObject(self, &bundleKey) as? String, let bundle = Bundle(path: path) else { return self.swizzled_localizedString(forKey: key, value: value, table: tableName) } return bundle.localizedString(forKey: key, value: value, table: tableName) } // 如果是系统Bundle,尝试从当前语言的lproj读取,否则用默认 guard let currentLang = currentLanguage, let path = Bundle.main.path(forResource: currentLang, ofType: "lproj"), let bundle = Bundle(path: path) else { return self.swizzled_localizedString(forKey: key, value: value, table: tableName) } // 系统控件的字符串可能存在于UIKit的lproj里,但我们可以用自己的本地化文件覆盖 let localizedString = bundle.localizedString(forKey: key, value: value, table: tableName) return localizedString != key ? localizedString : self.swizzled_localizedString(forKey: key, value: value, table: tableName) } }
2. 即时刷新所有UI控件
切换语言后,除了重新加载根视图控制器,我们还需要遍历当前界面的所有控件,强制刷新它们的本地化文本(比如已经存在的UILabel、UIButton、UINavigationItem等):
添加一个UIViewController和UIView的扩展来刷新控件:
extension UIViewController { func refreshLocalizedContent() { // 刷新当前控制器的视图 view.subviews.forEach { $0.refreshLocalizedContent() } // 刷新导航栏 navigationItem.title = navigationItem.title?.localized navigationItem.leftBarButtonItems?.forEach { btn in btn.title = btn.title?.localized } navigationItem.rightBarButtonItems?.forEach { btn in btn.title = btn.title?.localized } // 刷新TabBarItem(如果是TabBar控制器) if let tabBarItem = tabBarItem { tabBarItem.title = tabBarItem.title?.localized } } } extension UIView { func refreshLocalizedContent() { // 刷新UILabel if let label = self as? UILabel { label.text = label.text?.localized } // 刷新UIButton if let button = self as? UIButton { button.setTitle(button.title(for: .normal)?.localized, for: .normal) button.setTitle(button.title(for: .highlighted)?.localized, for: .highlighted) } // 刷新UITextField占位符 if let textField = self as? UITextField { textField.placeholder = textField.placeholder?.localized } // 递归刷新子视图 subviews.forEach { $0.refreshLocalizedContent() } } } // 添加String扩展,方便直接调用本地化 extension String { var localized: String { return NSLocalizedString(self, comment: "") } }
3. 修改语言切换方法
最后,更新你的languageButtonAction,添加控件刷新逻辑:
func languageButtonAction() { let targetLanguage = "hi" // 替换成你要切换的语言代码 // 设置语言 Bundle.setLanguage(targetLanguage) // 重新加载根视图控制器 let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let rootVC = storyboard.instantiateInitialViewController() else { return } UIApplication.shared.keyWindow?.rootViewController = rootVC // 刷新根控制器的所有控件 rootVC.refreshLocalizedContent() }
关键说明
- 方法交换(Method Swizzling)确保了UIKit系统控件的字符串读取也会走我们的自定义语言Bundle,无需重启。
- 控件刷新逻辑覆盖了常见的UI组件,你可以根据自己的项目需求扩展更多控件类型(比如
UISegmentedControl、UIPickerView等)。 - 记得把所有需要本地化的字符串用
String.localized方法包裹,这样切换语言时能自动更新。
内容的提问来源于stack exchange,提问作者Ankit Kumar Gupta
相关产品推荐
相关产品推荐

