美文网首页iOS学习专题iOS开发资料收集iOS开发
在iOS App内优雅的动态切换语言

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

作者: Dark_Angel | 来源:发表于2017-05-08 10:22 被阅读1019次

    前言

    随着公司业务的发展,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,是上面说的简化版本,大家有需要可以下载看下。里面有小福利哦,慢慢找吧~

    相关文章

      网友评论

      • 疯子_feng:楼主,我的项目结构是 UINavigationController->UITabBarController-> UINavigationController->设置->多语言 ,且自定义了UINavigationController的返回按钮只有一个箭头。 但是我每次切换语言时,都会变成系统的返回。 发现是rootviewcontroller 刷新出的问题。 请问下我这个要怎么来刷新 rootviewcontroller
      • poor_bird:楼主还在不,正在看,发现StoryboardAutoGenStrings在xib上不起作用,求解~
      • 00fce043cf44:如果个根控制器就一个UINavigationController控制器怎么写代码呢?
        ```
        //当前语言设置界面
        LanguageViewController *launageVC=[[LanguageViewController alloc]init];
        //获取到打开app进入主页的navbar控制器
        CZNavigationController *nvc = [AppDelegate shareInstance].navBar;
        NSMutableArray *vcs = nvc.viewControllers.mutableCopy;
        [vcs addObject:launageVC];
        //解决奇怪的动画bug。异步执行
        dispatch_async(dispatch_get_main_queue(), ^{

        [UIApplication sharedApplication].keyWindow.rootViewController = nvc;
        nvc.viewControllers = vcs;
        NSLog(@"已切换到语言 %@", [NSBundle currentLanguage]);
        });
        ```
        我发现这么写也只会会多加一个语言控制界面返回也没切换语言,请问如何修改呢?
      • 4f38d8742e72:demo 有 bug? 切换的时候明显 tabbar 有个动画移过去
        Dark_Angel:@Arvin_sir 之前没问题,很久不看了
      • 89f390658436:原来隐藏了一个自动化填充其他语言的 py
        Dark_Angel:@MartinLau_ OSX上在10.10之后NSWindow有一个新的属性:
        @property (nullable, strong) NSViewController *contentViewController NS_AVAILABLE_MAC(10_10);
        可以使用这个属性来刷新,10.10之前的似乎没有办法。
        具体的代码是:

        //先切换语言
        ...
        //然后重新加载window.contentViewController
        NSWindowController *windowController = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateInitialController];
        NSApp.keyWindow.contentViewController = windowController.contentViewController;
        :smiley:
        89f390658436:@Dark_Angel 借用来移植到 OSX 上了。不过似乎 OSX 没有办法像手机一样直接切换。看来还是要重启。:smile:
        Dark_Angel:@MartinLau_ 是用来方便国际化Storyboard的
      • overla5:你好。没用sb,纯代码写的,应该怎么切换所有的ui
        Dark_Angel:@失格人间 如果你问的是直接刷新app,你可以重新设置window的rootviewcontroller,思路是一样的
        Dark_Angel:@失格人间 你下载demo,我的页面里面有一个label,就是纯代码国际化
        Dark_Angel:@失格人间 写到strings文件中,其他一样,文中我有介绍到纯代码国际化
      • _Andy_:我也在做应用内切换语言 ,思路几乎和你的一样。但我的UINavigationController 的返回按钮一直无法正常切换,你可有好的思路吗?
        Dark_Angel:@大苏Andi Demo中我忽略这个问题,我更新了下Demo,你可以看下怎么实现的。

        1、使用Storyboard/Xib的话,需要在设置的上个页,比如我的页面,设置下navigationItem的Back Button(见Demo)。
        2、如果没有使用Storyboard或者Xib的话,就在我的页面的viewDidLoad里设置
        self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"代码设置返回", nil) style:UIBarButtonItemStylePlain target:nil action:nil]; 就好了。

        注意,这两者不要同时设置,不能共存的,你可以在Demo中试下。
        _Andy_:@Dark_Angel 返回按钮 也设置了多语言, 但是 点击切换后 ,返回按钮变成系统的 蓝色字体的系统按钮 比如:<返回 点击切换 变成了 <viewController 而无法实现切换,我看你的demo 上 也没有切换 返回按钮
        Dark_Angel:不是特别明白你的问题,是怎样无法正常切换?类似微信那样的bug吗?

      本文标题:在iOS App内优雅的动态切换语言

      本文链接:https://www.haomeiwen.com/subject/brgjtxtx.html