在iOS App内优雅的动态切换语言

前言

随着公司业务的发展,App版本的迭代,相信不少App都需要英文化(国际化)。App英文化,不外乎这三点:

  1. 纯代码中引用的strings国际化;
  2. Storyboard/Xib国际化;
  3. Info.plist国际化。

具体这三种分别如何操作,怎么国际化,这里不再赘述。一般App做了国际化,那么在用户切换手机系统Settings里的Language时,App就会切换成对应的语言(前提是做过该语言国际化)。那么如果想实现微博微信等App,在App内部实现切换语言,应该怎么做呢?如何做,才能更加优雅的动态切换语言呢?

下面,讲一讲如何在iOS App内优雅的动态切换语言。

调研

如果有相关需求,一般成熟的App都会怎么做呢?这里我们来看一下微博微信

微博

微信

对比一下可以看出,微博整体的效果比微信好很多,丝滑流畅。

这里注意一下细节:我在微博的个人中心是故意上滑了一下,然后点击进入设置,进行语言切换,可以看到切换时很自然,然后返回个人中心时,页面scrollViewcontentOffset并没有发生变化,由此可以推测:

<u>微博的思路是,在切换语言时,发送通知NSNotification,所有的UI控件监听通知,然后在适当的时候刷新UI</u>。那么其实这么写,需要做的东西很多,或是通过Base类来实现,或是通过runtime实现,总之ButtonLabelTextField等等都需要有一套统一的更新机制,可能不是一个最简单的办法。

<u>而微信切换的方案是,刷新keyWindowrootViewController,然后跳转到设置页,所以你会看到切换语言的一瞬间界面出现匪夷所思的bug,如下图</u>

其实微信的方案是个最简单的方案,只不过没有处理好这个小系统bug。

方案

核心方法

NSBundle

其实大家应该知道,无论是代码还是Storyboard/Xib,显示的国际化字符串都会走这个方法,传入一个key,获取localizedString

NSBundle的方法:

- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName;

比如我们常用的宏NSLocalizedString(@"done", nil)

#define NSLocalizedString(key, comment) \
        [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]

那么为何,系统切换语言的时候,此方法返回的就是对应语言(前提做了该语言的国际化)的字符串呢?原因在是哪个bundle调用了这个方法,一般默认的都是NSBundle.mainBundle这个对象。

 po [NSBundle allBundles]
 
<__NSArrayI 0x608000052f60>(
NSBundle </Users/DarkAngel/Library/Developer/CoreSimulator/Devices/CF831783-E4A6-4EC2-AB99-E04304331C3A/data/Containers/Bundle/Application/B391ED77-F57B-4B5E-9E53-CFD1245583D8/LanguageSettingsDemo.app/en.lproj> (not yet loaded),
NSBundle </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework/Artwork.bundle> (not yet loaded),
DABundle </Users/DarkAngel/Library/Developer/CoreSimulator/Devices/CF831783-E4A6-4EC2-AB99-E04304331C3A/data/Containers/Bundle/Application/B391ED77-F57B-4B5E-9E53-CFD1245583D8/LanguageSettingsDemo.app> (loaded)
)

可以看到有默认加载的bundle,还有个没加载的en bundle。所以,切换语言,其实只要切换了bundle对象,就可以让这个bundle根据key返回不同的localizedString

NSUserDefaults

其实语言设置,只要修改AppleLanguages对应的值就好了。这样才能加载正确语言的Storyboard/Xib,以及一些resources(图片之类的)。

实现思路

方案其实很简单,每次切换语言,把用户选择的语言保存在本地,同时更改bundle对象,然后刷新页面就可以了。

技术细节

保存用户设置

很简单,保存在NSUserDefaults里。

这里需要说明的是跟随手机系统,即清除用户自定义设置,只需要将AppleLanguages字段设为nil即可。

但是当AppleLanguages字段设为nil,你再去获取它的值时,会发现他已经变成了系统语言的默认值。这里可以参考这篇文章

.h

/**
 设置类
 */
@interface UWConfig : NSObject
/**
 用户自定义使用的语言,当传nil时,等同于resetSystemLanguage
 */
@property (class, nonatomic, strong, nullable) NSString *userLanguage;
/**
 重置系统语言
 */
+ (void)resetSystemLanguage;
@end

.m

#import "UWConfig.h"

static NSString *const UWUserLanguageKey = @"UWUserLanguageKey";
#define STANDARD_USER_DEFAULT  [NSUserDefaults standardUserDefaults]

@implementation UWConfig
+ (void)setUserLanguage:(NSString *)userLanguage
{
    //跟随手机系统
    if (!userLanguage.length) {
        [self resetSystemLanguage];
        return;
    }
    //用户自定义
    [STANDARD_USER_DEFAULT setValue:userLanguage forKey:UWUserLanguageKey];
    [STANDARD_USER_DEFAULT setValue:@[userLanguage] forKey:@"AppleLanguages"];
    [STANDARD_USER_DEFAULT synchronize];
}

+ (NSString *)userLanguage
{
    return [STANDARD_USER_DEFAULT valueForKey:UWUserLanguageKey];
}

/**
 重置系统语言
 */
+ (void)resetSystemLanguage
{
    [STANDARD_USER_DEFAULT removeObjectForKey:UWUserLanguageKey];
    [STANDARD_USER_DEFAULT setValue:nil forKey:@"AppleLanguages"];
    [STANDARD_USER_DEFAULT synchronize];
}

@end

在需要的地方调用即可,如

   if (indexPath.row == 0) {
        [UWConfig setUserLanguage:nil];
    } else if (indexPath.row == 1) {
        [UWConfig setUserLanguage:@"zh-Hans"];
    } else {
        [UWConfig setUserLanguage:@"en"];
    }

切换Bundle

当需要展示内容时,才需要用到bundle,我们要手动切换bundle对象,就用到了这几个方法:

+ (nullable instancetype)bundleWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

+ (nullable instancetype)bundleWithURL:(NSURL *)url NS_AVAILABLE(10_6, 4_0);
- (nullable instancetype)initWithURL:(NSURL *)url NS_AVAILABLE(10_6, 4_0);

这里直接上代码

.h

@interface NSBundle (UWUtils)

+ (BOOL)isChineseLanguage;

+ (NSString *)currentLanguage;

@end

.m

#import "NSBundle+UWUtils.h"

@interface UWBundle : NSBundle

@end

@implementation NSBundle (UWUtils)

+ (BOOL)isChineseLanguage
{
    NSString *currentLanguage = [self currentLanguage];
    if ([currentLanguage hasPrefix:@"zh-Hans"]) {
        return YES;
    } else {
        return NO;
    }
}

+ (NSString *)currentLanguage
{
    return [UWConfig userLanguage] ? : [NSLocale preferredLanguages].firstObject;
}

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //动态继承、交换,方法类似KVO,通过修改[NSBundle mainBundle]对象的isa指针,使其指向它的子类UWBundle,这样便可以调用子类的方法;其实这里也可以使用method_swizzling来交换mainBundle的实现,来动态判断,可以同样实现。
        object_setClass([NSBundle mainBundle], [UWBundle class]);
    });
}

@end

@implementation UWBundle

- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
    if ([UWBundle uw_mainBundle]) {
        return [[UWBundle uw_mainBundle] localizedStringForKey:key value:value table:tableName];
    } else {
        return [super localizedStringForKey:key value:value table:tableName];
    }
}

+ (NSBundle *)uw_mainBundle
{
    if ([NSBundle currentLanguage].length) {
        NSString *path = [[NSBundle mainBundle] pathForResource:[NSBundle currentLanguage] ofType:@"lproj"];
        if (path.length) {
            return [NSBundle bundleWithPath:path];
        }
    }
    return nil;
}

@end

这里涉及到了runtime的使用,代码中有注释,这里就不展开了。

刷新页面

用微信的思路,简单化,不必要处理很多,我们只要替换keyWindowrootViewController就好,同时解决微信的bug。

    UITabBarController *tbc = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateInitialViewController];
    //我这里的storyboard为了便于多人合作,这里只包含根tabBarController和多个nvc,每个nvc只有自己的rootViewController
    //跳转到个人中心
    tbc.selectedIndex = 4;
    //创建设置页面
    UWSettingViewController *vc1 = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:NSStringFromClass([UWSettingViewController class])];
    vc1.hidesBottomBarWhenPushed = YES;
    //创建语言切换页
    UWLanguageSettingsViewController *vc2 = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:NSStringFromClass([UWLanguageSettingsViewController class])];
    vc2.hidesBottomBarWhenPushed = YES;
    UINavigationController *nvc = tbc.selectedViewController;
    //备用
    NSMutableArray *vcs = nvc.viewControllers.mutableCopy;
    [vcs addObjectsFromArray:@[vc1, vc2]];
    //解决奇怪的动画bug。异步执行
    dispatch_async(dispatch_get_main_queue(), ^{
        //注意刷新rootViewController的时机,在主线程异步执行
        //先刷新rootViewController
        [UIApplication sharedApplication].keyWindow.rootViewController = tbc;
        //然后再给个人中心的nvc设置viewControllers
        nvc.viewControllers = vcs;
        //一些UI提示,可以提供更友好的用户交互(也可以删掉)
        [UWProgressHUD showLoadingWithMessage:NSLocalizedString(UWSettingMessage, nil)];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [UWProgressHUD dismiss];
        });
    });

整体下来,就可以实现在App内优雅的切换语言。

总结

其实在实践过程中,坑还是很多的,欢迎提问。

下面附上个简单的Demo,是上面说的简化版本,大家有需要可以下载看下。里面有小福利哦,慢慢找吧~

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

推荐阅读更多精彩内容

  • 定场诗 锄禾日当午,码农真辛苦。对着小屏幕,周一到周五。额,突发奇想,想写首诗,不要问我为什么这么有才华。 目标 ...
    翻炒吧蛋滚饭阅读 17,610评论 27 43
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 当我们的应用仅仅面向国内用户群,一般仅支持一种语言--中文就可以了。当面向国外用户时就需要进行国际化了,不仅仅是语...
    FarmGuo阅读 33,151评论 25 82
  • 这个 巷子被伪装门隔断 被分离了两边的村庄 有陌生男人女人的注视目光 有略显刺眼的保安的手电筒光 残垣 断壁 阴冷...
    婉滢Sophia阅读 157评论 0 0
  • 今日早餐,丰盛有营养。 最近与女儿总是生气,计划也没有跟她谈,女儿说我不尊重她,是啊,看见她玩,想想琴还没有弹就生...
    影子3623253阅读 104评论 0 1