iOS中APP适配(RTL阿拉伯适配)

最近项目适配阿拉伯,记录一下最近的工作内容。在此之前,我是没有了解过这方面的知识。
首先说说为什么要适配阿拉伯呢,是因为我们中文和英文这些是从左往右显示的语言,但是阿拉伯的语言是从右往左显示(RTL),恰好与我们的习惯相反,刚开始的时候实在很别扭,
首先在适配的项目的开始,我查找了一下网上的资料
感谢这几位大佬的博客:
https://blog.csdn.net/a657651096/article/details/102805114
https://www.jianshu.com/p/042f3db234ad
https://www.jianshu.com/p/3383ca5f6de0
我的项目是OC开发,布局用的masonry。
先来捋一下阿拉伯适配需要做哪些事情呢。
1阿拉伯从右往左显示,我们所有的约束需要更换。
2所有的UIView的处理
3带方向的图片处理
4手势的处理
5文字显示方向TextAlignment(大部分是UILabel)
6UIEdgeInsets(UIButton)
7富文本AttributeString
8Unicode文字的处理
9UICollectionView的处理(水平方向的)
10UIScrollView的处理(水平方向)

我们先来看一组效果图:
这是在中文下的效果


image.png

这是阿拉伯下的效果


image.png

列了一下需要处理的问题列表,接下来就是解决问题的具体方案了:
我写了一个公共的宏定义判断是不是阿拉伯语言,这个地方可以根据不同的需求做判断

#define isRTL() [[[[NSBundle mainBundle] preferredLocalizations] firstObject] hasPrefix:@"ar"]

1将所有的left更换成leading,right更换成trailing,这至少解决了50%的问题。是不是非常简单NONONO~~

全部UIView处理
iOS9之后,苹果出了API适配RTL
UIView有一个semanticContentAttribute的属性,当我们将其设置成UISemanticContentAttributeForceRightToLeft之后,UIView将强制变为RTL布局。当然在非RTL语言下,我们需要设置它为UISemanticContentAttributeForceLeftToRight,来适配系统是阿拉伯语,App是其他语言不需要RTL布局的情况。
项目中有无数个UIView,是不是需要我们一个一个去设置呢,当然不是,这时候大家想到的是不是hook一下UIView的方法,来达到效果呢,好像是不行的呢,原因可以看我前面提到的三位的博客,我在appdelegate里面统一设置的,当我们设置UIView的semanticContentAttribute以后,发现UISearchBar还没有改变,那我们再设置一下UISearchBar

 if (isRTL()) {
        [UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
        [UISearchBar appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
    }

处理带方向的图片,这个部分有两种方式可以处理,要么让UI切两套图,分别展示,或者是把图片翻转一下,当然,图片不能带文字,这里得多说一句,经过这一次的教训,我发誓以后再也不要用带文字的图片了,如果只是带方向的图片,翻转就行了,但是图片带文字那就玩不转了,只能用几套图,还有国际化的时候,图片带文字,也不好处理,很不幸,我项目中很多带文字的图片,我只能一个一个去修改,言归正传,先来看一下处理带方向图片处理:
给UIImage写了一个分类,添加了一个方法,在方法里面判断是不是阿拉伯语,如果是翻转了图片,翻转图片的方法用的系统自带的。

#import "UIImage+HALFlipped.h"

@implementation UIImage (HALFlipped)
- (UIImage *)hal_imageFlippedForRightToLeftLayoutDirection
{
    if (isRTL()) {
        return [UIImage imageWithCGImage:self.CGImage
                                   scale:self.scale
                             orientation:UIImageOrientationUpMirrored];
    }

    return self;
}
@end

这样子在带方向的地方使用这个方法,就可以了


 UIButton * backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
  [backBtn setImage:[[UIImage imageNamed:@"kls_Room_Box_back"]  hal_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];

手势的处理
滑动返回
RTL下,除了布局需要调整,手势的方向也是需要调整的
正常的滑动返回手势是右滑,在RTL下,是需要变成左滑返回的。为了让滑动返回也适配RTL,我们需要修改navigationBar和UINavigationController.view的semanticContentAttribute。使用[UIView appearance]修改semanticContentAttribute并不能使手势随之改变,我们需要手动修改。为了让所有的UINavigationController都生效。我们hook了UINavigationController的initWithNibName:bundle:

#import "UINavigationController+HALRTL.h"

@implementation UINavigationController (HALRTL)
+ (void)load
{
    if (isRTL()) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method oldMethod = class_getInstanceMethod(self, @selector(initWithNibName:bundle:));
            Method newMethod = class_getInstanceMethod(self, @selector(rtl_initWithNibName:bundle:));
            method_exchangeImplementations(oldMethod, newMethod);
        });
    }
}

- (instancetype)rtl_initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
    if ([self rtl_initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        if (@available(iOS 9.0, *)) {
            self.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;;
            self.view.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
        }
    }
    return self;
}
@end

其他手势

跟方向有关的手势有2个:UISwipeGestureRecognizer和UIPanGestureRecognizer

UIPanGestureRecognizer是无法直接设置有效方向的。为了设置只对某个方向有效,一般都是通过实现它的delegate中的gestureRecognizerShouldBegin:方法,来指定是否生效。对于这种情况,我们只能手动修gestureRecognizerShouldBegin:中的逻辑,来适配RTL

UISwipeGestureRecognizer有一个direction的属性,可以设置有效方向。为了适配RTL,我们可以hook它的setter方法,达到自动适配的目的:

#import "UISwipeGestureRecognizer+HALRTL.h"

@implementation UISwipeGestureRecognizer (HALRTL)

+ (void)load
{
    Method oldAttMethod = class_getInstanceMethod(self,@selector(setDirection:));
    Method newAttMethod = class_getInstanceMethod(self,@selector(rtl_setDirection:));
    method_exchangeImplementations(oldAttMethod, newAttMethod);  //交换成功
   
}

- (void)rtl_setDirection:(UISwipeGestureRecognizerDirection)direction
{
    
    if (isRTL()) {
        if (direction == UISwipeGestureRecognizerDirectionRight) {
            direction = UISwipeGestureRecognizerDirectionLeft;
        } else if (direction == UISwipeGestureRecognizerDirectionLeft) {
            direction = UISwipeGestureRecognizerDirectionRight;
        }
    }
    [self rtl_setDirection:direction];
}
@end

UIButton的RTL适配
UIButton的imageEdgeInsets和titleEdgeInsets。正常的时候,我们设置一个titleEdgeInsets的left。但是当RTL的情况下,因为所有的东西都左右镜像了,应该设置titleEdgeInsets的right布局才会正常。然而系统却不会自动帮我们将left和right调换。我们需要手动去适配它。

@implementation UIButton (HALRTL)

UIEdgeInsets RTLEdgeInsetsWithInsets(UIEdgeInsets insets) {
    if (insets.left != insets.right && isRTL()) {
        CGFloat temp = insets.left;
        insets.left = insets.right;
        insets.right = temp;
    }
    return insets;
}


+ (void)load{
    if (isRTL()) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method oldMethod = class_getInstanceMethod(self, @selector(setContentEdgeInsets:));
            Method newMethod = class_getInstanceMethod(self, @selector(rtl_setContentEdgeInsets:));
            method_exchangeImplementations(oldMethod, newMethod);
            
            Method oldImageMethod = class_getInstanceMethod(self, @selector(setImageEdgeInsets:));
            Method newImageMethod = class_getInstanceMethod(self, @selector(rtl_setImageEdgeInsets:));
            method_exchangeImplementations(oldImageMethod,newImageMethod);
            
            Method oldTitleMethod = class_getInstanceMethod(self, @selector(setTitleEdgeInsets:));
            Method newTitleMethod = class_getInstanceMethod(self, @selector(rtl_setTitleEdgeInsets:));
            method_exchangeImplementations(oldTitleMethod,newTitleMethod);
        });
    }
}

- (void)rtl_setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
    [self rtl_setContentEdgeInsets:RTLEdgeInsetsWithInsets(contentEdgeInsets)];
}

- (void)rtl_setImageEdgeInsets:(UIEdgeInsets)imageEdgeInsets {
    [self rtl_setImageEdgeInsets:RTLEdgeInsetsWithInsets(imageEdgeInsets)];
}

- (void)rtl_setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets {
    [self rtl_setTitleEdgeInsets:RTLEdgeInsetsWithInsets(titleEdgeInsets)];
}

@end

TextAlignment
RTL下textAlignment也是需要调整的,官方文档中默认textAlignment是NSTextAlignmentNatural,并且NSTextAlignmentNatural可用自动适配RTL
然而,情况并没有文档描述的那么好,当我们在系统内切换语言的时候,系统经常会错误的设置textAlignment。没有办法,我们只有自己去适配textAlignment.

以UILabel为例,我们hook它的setter的方法,根据当前是否是RTL,来设置正确的textAlignment,如果UILabel从未调用setTextAlignment:,我们还需要给它一个正确的默认值。

#import "UILabel+HALRTL.h"
@implementation UILabel (HALRTL)

+ (void)load
{
    
    Method oldInitMethod = class_getInstanceMethod(self,@selector(initWithFrame:));
    Method newInitMethod = class_getInstanceMethod(self, @selector(rtl_initWithFrame:));
    method_exchangeImplementations(oldInitMethod, newInitMethod);  //交换成功
    
    Method oldTextMethod = class_getInstanceMethod(self,@selector(setTextAlignment:));
    Method newTextMethod = class_getInstanceMethod(self, @selector(rtl_setTextAlignment:));
    method_exchangeImplementations(oldTextMethod, newTextMethod);  //交换成功
}

- (instancetype)rtl_initWithFrame:(CGRect)frame
{
    if ([self rtl_initWithFrame:frame]) {
        self.textAlignment = NSTextAlignmentNatural;
    }
    return self;
}

- (void)rtl_setTextAlignment:(NSTextAlignment)textAlignment
{
    if (isRTL()) {
        if (textAlignment == NSTextAlignmentNatural || textAlignment == NSTextAlignmentLeft) {
            textAlignment = NSTextAlignmentRight;
        } else if (textAlignment == NSTextAlignmentRight) {
            textAlignment = NSTextAlignmentLeft;
        }
    }
    [self rtl_setTextAlignment:textAlignment];
}

富文本AttributeString和Unicode字符串

以UILabel为例,对于AttributeString,UILabel的textAlignment是不生效的,因为AttributeString自带attributes。为了让attributeString也能自动适配RTL。我们需要在RTL下,将Alignment的left和right互换。
attributeString的alignment一般使用NSMutableParagraphStyle设置,所以我们首先hook NSMutableParagraphStyle,在setAlignment的时候设上正确的alignment:

由于阅读习惯的差异(阿拉伯语从右往左阅读,其他语言从左往右阅读),所以字符的排序是不一样的,普通语言左边是第一个字符,阿拉伯语右边是第一个字符。

如果是单纯某种文字,不管是阿拉伯语还是英文,系统都是已经帮助我们做好适配了的。然而混排的情况下,系统的适配是有问题的。对于一个string,系统会用第一个字符来决定当前是LTR还是RTL。

那么坑来了,假设有一个这样的字符串@"小明بدأ في متابعتك"(翻译过来为:小明关注了你),在阿拉伯语的情况下,由于阅读顺序是从右往左,我们希望他显示为@"بدأ في متابعتك小明"。然而按照系统的适配方案,是永远无法达到我们期望的。

如果"小明"放前面,第一个字符是中文,系统识别为LTR,从左往右排序,显示为@"小明بدأ في متابعتك"。
如果"小明"放后面,第一个字符是阿拉伯语,系统识别为RTL,从右往左排序,依然显示为@"小明بدأ في متابعتك"。
为了适配这种情况,可以在字符串前面加一些不会显示的字符,强制将字符串变为LTR或者RTL。

在字符串前面添加"\u202B"表示RTL,加"\u202A"LTR。为了统一适配刚刚的情况,我们hook了UILabel的setText:方法。当然这种方法没法适配所有的情况,项目中具体的场景还得具体处理。

#import "UILabel+HALAttrRTL.h"

BOOL isRTLString(NSString *string) {
    if ([string hasPrefix:@"\u202B"] || [string hasPrefix:@"\u202A"]) {
        return YES;
    }
    return NO;
}

NSString * RTLString(NSString *string) {
    if (string.length == 0 || isRTLString(string)) {
        return string;
    }
    if (isRTL()) {
        string = [@"\u202B" stringByAppendingString:string];
    } else {
        string = [@"\u202A" stringByAppendingString:string];
    }
    return string;
}

NSAttributedString *RTLAttributeString(NSAttributedString *attributeString ){
    if (attributeString.length == 0) {
        return attributeString;
    }
    NSRange range;
    NSDictionary *originAttributes = [attributeString attributesAtIndex:0 effectiveRange:&range];
    NSParagraphStyle *style = [originAttributes objectForKey:NSParagraphStyleAttributeName];
    
    if (style && isRTLString(attributeString.string)) {
        return attributeString;
    }
    
    NSMutableDictionary *attributes = originAttributes ? [originAttributes mutableCopy] : [NSMutableDictionary new];
    if (!style) {
        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
        UILabel *test = [UILabel new];
        test.textAlignment = NSTextAlignmentLeft;
        mutableParagraphStyle.alignment = test.textAlignment;
        style = mutableParagraphStyle;
        [attributes setValue:mutableParagraphStyle forKey:NSParagraphStyleAttributeName];
    }
    NSString *string = RTLString(attributeString.string);
    return [[NSAttributedString alloc] initWithString:string attributes:attributes];
}

@implementation UILabel (HALAttrRTL)

+(void)load{

    Method oldAttMethod = class_getInstanceMethod(self,@selector(setAttributedText:));
    Method newAttMethod = class_getInstanceMethod(self, @selector(rtl_setAttributedText:));
    method_exchangeImplementations(oldAttMethod, newAttMethod);  //交换成功
    
    Method oldTextMethod = class_getInstanceMethod(self,@selector(setText:));
    Method newTextMethod = class_getInstanceMethod(self,@selector(rtl_setText:));
    method_exchangeImplementations(oldTextMethod, newTextMethod);  //交换成功
}

- (void)rtl_setAttributedText:(NSAttributedString *)attributedText
{
    if (isRTL()) {
        attributedText = RTLAttributeString(attributedText);
       

    }
    [self rtl_setAttributedText:attributedText];
}

- (void)rtl_setText:(NSString *)text
{
    [self rtl_setText:RTLString(text)];
}
@end

以上是常见的适配了,接下来说两个特殊的

UICollectionView在RTL下的适配
继承UICollectionViewFlowLayout 重写两个方法

-(UIUserInterfaceLayoutDirection)effectiveUserInterfaceLayoutDirection {

    if (isRTL()) {
        return UIUserInterfaceLayoutDirectionRightToLeft;
    }
    return UIUserInterfaceLayoutDirectionLeftToRight;
}

- (BOOL)flipsHorizontallyInOppositeLayoutDirection{
    return  YES;
}

最后UIScrollView在RTL适配

普通的UIScrollView可以通过把UIScrollView的transform和scrollView的subviews翻转一下

    if (isRTL()) {
        self.scrollView.transform = CGAffineTransformMakeRotation(M_PI);
        NSArray *subViews = self.scrollView.subviews;
        for (UIView *subView in subViews) {
            if ([subView isKindOfClass:[HALUserInfoRelationshipView class]]) {
                subView.transform = CGAffineTransformMakeRotation(M_PI);
            }
        }
    }

我项目中太多的地方用到了UIScrollView,因为我们的UI设计,有非常多的分页控制器,所以我们项目中使用JXCategory搭配UIScrollView。
在使用的过程中遇到一个小问题,例如ScrollView加载三个不同的view,每个view的宽度都是屏幕的宽,这在RTL下有个问题,就是有view不显示,我从左往右适配的时候,右边的不显示,从右往左适配,左边的不显示,后来我使用了一种比较愚蠢的方法。最左的view从左适配,最右的View从右适配

   self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 85 * SCREEN_SCALEIPhone6, kScreenWidth, 233 * SCREEN_SCALEIPhone6)];
    self.scrollView.pagingEnabled = YES;
    self.scrollView.bounces = NO;
    self.scrollView.showsVerticalScrollIndicator = NO;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.backgroundColor = UIColor.clearColor;
    [self.bottomView addSubview:self.scrollView];
    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.mas_equalTo(self.bottomView.mas_leading);
        make.top.mas_equalTo(self.userListView.mas_bottom);
        make.width.mas_equalTo(ScreenWidth);
        make.height.mas_equalTo(233*SCREEN_SCALEIPhone6);
    }];
    self.scrollContentView = [UIView new];
    [self.scrollView addSubview:self.scrollContentView];
    
    [self.titleArray addObject:klstring(@"label_410")];
    HALRoomTempGiftView *giftView = [[HALRoomTempGiftView alloc] initWithRoomId:self.room_id GiftRoomType:self.gift_roomType IsBackPack:YES TagId:@"0"];
    [self.scrollContentView addSubview:giftView];
    [giftView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(0);
        if (isRTL()) {
            make.trailing.mas_equalTo(0);
        }else{
            make.leading.mas_equalTo(0);
        }
        make.height.mas_equalTo(245*SCREEN_SCALEIPhone6);
        make.width.mas_equalTo(ScreenWidth);
    }];

       HALRoomTempGiftView *giftView = [[HALRoomTempGiftView alloc] initWithRoomId:self.room_id GiftRoomType:self.gift_roomType IsBackPack:NO TagId:tempModel.tag_id];
            [self.scrollContentView addSubview:giftView];
            [giftView mas_makeConstraints:^(MASConstraintMaker *make) {
                make.top.mas_equalTo(self.scrollView.mas_top);
                if (isRTL()) {
                    make.leading.mas_equalTo(ScreenWidth *i);
                }else{
                    make.leading.mas_equalTo(ScreenWidth*(i+1));
                }
               
                make.height.mas_equalTo(245*SCREEN_SCALEIPhone6);
                make.width.mas_equalTo(ScreenWidth);
            }];
            @weakify(self);
            giftView.spec_listBlock = ^(NSArray * _Nonnull array) {
                @strongify(self);
                self.selectedGiftCountNumber = [array firstObject];
                self.numberView.spec_list = array;
                [self.selectedNumberBtn configWithNumber:self.selectedGiftCountNumber];
            };
            
            giftView.didSelectGiftMoldeBlock = ^(EBCounterItemModel * _Nonnull model) {
                @strongify(self);
                self.selectedGiftModel = model;
            };
            [self.giftViews addObject:giftView];
            if (i == 0) {
                [giftView configurationGiftListData];
            }
        }
        
        self.giftTitleView.titles = self.titleArray;
        [self.scrollContentView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.trailing.top.mas_equalTo(0);
            make.width.mas_equalTo(ScreenWidth * self.titleArray.count);
            make.height.mas_equalTo(233 * SCREEN_SCALEIPhone6);
        }];
        self.scrollView.contentSize = CGSizeMake(kScreenWidth*self.titleArray.count, 0);
        self.giftTitleView.contentScrollView = self.scrollView;

我的RTL适配之路暂时就到这里了,希望未来有更好的方案出现。不安的2020即将完毕,祝愿2021是温暖的一年。

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

推荐阅读更多精彩内容