You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需重启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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:57:25