Dark Angel

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

次阅读
本文目录
  1. 前言
  2. 调研
  3. 方案
    1. 核心方法
      1. NSBundle
      2. NSUserDefaults
    2. 实现思路
    3. 技术细节
      1. 保存用户设置
      2. 切换Bundle
      3. 刷新页面
  4. 总结

前言

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

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

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

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

调研

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

微博

微信

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

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

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

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

其实微信的方案是个最简单的方案,只不过没有处理好这个小系统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,是上面说的简化版本,大家有需要可以下载看下。里面有小福利哦,慢慢找吧~

最后,优客工场欢迎您。

Dark Angel

iOS界的低调探索者

 
Powered By Hexo