runtime 在应用国际化上的实践

959 阅读3分钟

前言

在应用开发时,某一天我们的产品经理兴高采烈的和我们说:“我们的产品即将走向国际化,我们要做美国,英国,德国…进行推广。” 这意味着我们需要做国际化版本了。

查看图片
怎么会这样。。

我们的代码经常会有下面这样的代码

或者是在直接在 xib、StoryBoard 直接设置属性了。

国际化

在iOS开发,我们是如何实现国际化的呢?对这一块不了解的同学可以看这篇文章

虽然 Xib、StoryBoard 都可以设置国际化。但我们
还是习惯全部写在一个 strings 中,这样方便做翻译的同学进行翻译。

那这样我们要进行国际化的流程是

查看图片

这样实在太烦了。。这么多控件。。

使用runtime解决问题

setText 国际化

国际化主要的工作就是在 setText 之前需要调用 NSLocalizedString 生成国际化后的字符串。

目前代码使我们纠结的地方是我们就直接使用 setText 了。我们希望在setText时插入一段国际化的代码。

我们希望在执行某个函数之前插入一段代码,Runtime的 Method Swizzling 可以实现这样的功能。

@implementation UILabel(NewLabel)
+ (void)load {
    [UILabel configSwizzled];
    + (void)configSwizzled {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(setText:);
        SEL swizzledSelector = @selector(setNewText:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
    });
    - (void)setNewText:(NSString *)text {
    [self setNewText:NSLocalizedString(text, nil)];
    @end
  • 我们使用分类扩展 UILabel
  • 然后重写 load 这个函数,在里面进行Swizzle的初始化。
  • 在这里我们把 setText Swizzle setNewText.
  • setNewText 中我们我们调用 NSLocalizedString 进行国际化处理。

好了,这样我们解决了在代码中 setText 的国际化问题。

Xib StoryBoard 国际化

这里我们发现,Xib StoryBoard 中设置属性的控件不会调用 setText

那这我们怎么解决呢? 让他们调用一下 setText 吧。那我们需要怎么做? Xib StoryBoard 的控件,必然会走 initWithCoder 这个初始化函数。我们在再次使用 Runtime 的黑魔法,让 initWithCoder 执行完后,我们在调用一下 setText

直接看代码吧:

+ (void)configSwizzled {
...
dispatch_once(&onceToken2, ^{
        Class class = [self class];
        SEL originalSelector = ;
        SEL swizzledSelector = ;
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
    });
    - (instancetype)initNewWithCoder:(NSCoder *)aDecoder {
    id result = [self initNewWithCoder:aDecoder];
    [self setText:self.text];
    return result;

So easy !!

不用进行国际化的控件怎么办

我们可以添加一个变量来控制代码是否进行国际化。那就使用关联对象(Associated Object)吧。

@interface UILabel (NewLabel)
@property (nonatomic, assign)IBInspectable BOOL localizedEnlabe;
@end
@implementation UILabel(NewLabel)
static char *localizedEnlabeChar = "LocalizedEnlabe";
- (void)setLocalizedEnlabe:(BOOL)localizedEnlabe {
    objc_setAssociatedObject(self, &localizedEnlabeChar, [NSNumber numberWithBool:localizedEnlabe], OBJC_ASSOCIATION_ASSIGN);
    - (BOOL)localizedEnlabe {
    NSNumber *value = objc_getAssociatedObject(self, &localizedEnlabeChar);
    if (value) {
        return [value boolValue];
    return YES;
    @end
  • 这里我使用 IBInspectable 属性方便 Xib StoryBoard 设置属性.

总结

这只是个 Demo, 需要国际化的控件还有 UITextFieldUIButton 等控件。其实我们这些代码可以直接在 UIView 的分类中实现。

然后把我们要处理的属性方法以同样的方式 Swizzle 。

虽然这种方法不见得能解决所有问题,但应该是可以解决 80% 的问题的。