前言

阿拉伯地区的语言是从右到左展示的,不止是文本的适配,页面布局也需要做适配。
iOS9之后的RTL适配,官方文档做适配

适配

UIVIew

1
2
3
4
5
6
7
8
9
typedef NS_ENUM(NSInteger, UISemanticContentAttribute) {
UISemanticContentAttributeUnspecified = 0,
UISemanticContentAttributePlayback, // for playback controls such as Play/RW/FF buttons and playhead scrubbers
UISemanticContentAttributeSpatial, // for controls that result in some sort of directional change in the UI, e.g. a segmented control for text alignment or a D-pad in a game
UISemanticContentAttributeForceLeftToRight,
UISemanticContentAttributeForceRightToLeft
} NS_ENUM_AVAILABLE_IOS(9_0);

@property (nonatomic) UISemanticContentAttribute semanticContentAttribute NS_AVAILABLE_IOS(9_0);

设置 UIView 的属性 semanticContentAttribute 解决,UIView是所有控件的基类,不可能给项目所有的控件都进行修改,想要使用hook方法在初始化方法中解决,但是会有坑,比如 WKWebView
AppDelegate 中使用 [UIView appearance] 解决

1
2
3
4
if (kIsRTL()) {
[UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
[UISearchBar appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}

布局

Masonry

left、right 替换成 leading、trailing

全局搜索 .left..right.mas_leftmas_right
image.png|500

注意:要Masonry库中的内容不能被替换

Frame

使用 frame 的布局需要我们自行处理适配,使用 category处理:
RTL的布局 对于 ysize 是不会影响,唯一要处理的是 frame.origin.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)setRTLFrame:(CGRect)frame width:(CGFloat)width {

if (kIsRTL()) {
if (self.superview == nil) {
NSAssert(0, @"must invoke after have superView");
}
CGFloat x = width - frame.origin.x - frame.size.width;
frame.origin.x = x;
}
self.frame = frame;
}

- (void)setRTLFrame:(CGRect)frame {

[self setRTLFrame:frame width:self.superview.frame.size.width];
}

- (void)resetFrameToFitRTL {

[self setRTLFrame:self.frame];
}

  • 对于旧代码中的视图 Frame 且 父视图的frame已知,可以直接使用 resetFrameToFitRTL 方法
  • 对于新代码可以考虑直接使用 Masonry 布局
  • 特殊情况就要特殊处理,比如Masonry布局影响性能,使用Frame布局

contentHorizontalAlignment

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
UIControlContentHorizontalAlignmentCenter = 0,
UIControlContentHorizontalAlignmentLeft = 1,
UIControlContentHorizontalAlignmentRight = 2,
UIControlContentHorizontalAlignmentFill = 3,
UIControlContentHorizontalAlignmentLeading API_AVAILABLE(ios(11.0), tvos(11.0)) = 4,
UIControlContentHorizontalAlignmentTrailing API_AVAILABLE(ios(11.0), tvos(11.0)) = 5,
};

UIControlContentHorizontalAlignmentLeftUIControlContentHorizontalAlignmentRight 换成 UIControlContentHorizontalAlignmentLeadingUIControlContentHorizontalAlignmentTrailing

TextAlignment

官方中 textAlignment ios9 系统之后,默认是使用 NSTextAlignmentNatural,这个属性也是会自动适配RTL

UITextField

UITextFileld 中的书写方向会根据键盘的语言,当前编辑的内容自动适配

UILabel

虽然系统默认使用 NSTextAlignmentNatural 适配方向,还是需要手动设置的方向适配
使用 交换方法 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static char kAssociatedObjectKeyLabelTextAlignment;

- (NSTextAlignment)rtl_textAlignment {

return [objc_getAssociatedObject(self, &kAssociatedObjectKeyLabelTextAlignment) integerValue];
}

- (void)setRtl_textAlignment:(NSTextAlignment)rtl_textAlignment {

objc_setAssociatedObject(self, &kAssociatedObjectKeyLabelTextAlignment, @(rtl_textAlignment), OBJC_ASSOCIATION_ASSIGN);

if (kIsRTL) {
if (rtl_textAlignment == NSTextAlignmentNatural || rtl_textAlignment == NSTextAlignmentLeft) {
rtl_textAlignment = NSTextAlignmentRight;
} else if (rtl_textAlignment == NSTextAlignmentRight) {
rtl_textAlignment = NSTextAlignmentLeft;
}
}
self.textAlignment = rtl_textAlignment;
}

文本拼接混排的情况

阅读习惯的不同要区分文本拼接是左到右,还是右到左

image.png|500
根据首个字符是什么语言,文本的方向就会自动改变

比如:”天气是 المطر”

  • “天气是المطر”,”天气” 提到前面就是 LTR 的顺序,APP显示为:”天气是 المطر”
  • “المطر天气是”,” المطر” 放到前面就是 RTL 的顺序,APP显示为:”天气是 المطر”

为了适配这种情况,可以在字符串前面加一些不会显示的字符,强制将字符串变为LTR或者RTL。

在字符串前面添加\u202B表示RTL,加\u202A 表示LTR

iOS 将@也当成了阿拉伯语 的一部分,我们需要对@手动添加 LEFT-TO-RIGHT 标志 \u200E,声明为LTR展示 @"\u200E@"

hook UILabel 中设置文本方向处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 (kIsRTL()) {
string = [@"\u202B" stringByAppendingString:string];
} else {
string = [@"\u202A" stringByAppendingString:string];
}
return string;
}

@implementation UILabel (RTL)

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

NSMutableParagraphStyle

方法1:对于富文本直接设置 UILabel 的 textAlignment 无效,要使用 NSMutableParagraphStyle 设置 textAlignment,可以 hook NSMutableParagraphStyle 设置正确的 textAlignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@implementation NSMutableParagraphStyle (RTL)

static char kAssociatedObjectKeyParagraphStyleAlignment;

- (NSTextAlignment)rtl_alignment {

return [objc_getAssociatedObject(self, &kAssociatedObjectKeyParagraphStyleAlignment) integerValue];
}

- (void)setRtl_alignment:(NSTextAlignment)rtl_alignment {

objc_setAssociatedObject(self, &kAssociatedObjectKeyParagraphStyleAlignment, @(rtl_alignment), OBJC_ASSOCIATION_ASSIGN);

if (kIsRTL) {
if (rtl_alignment == NSTextAlignmentLeft || rtl_alignment == NSTextAlignmentNatural) {
rtl_alignment = NSTextAlignmentRight;
} else if (rtl_alignment == NSTextAlignmentRight) {
rtl_alignment = NSTextAlignmentLeft;
}
}
self.alignment = rtl_alignment;
}

@end

UIImage

系统提供了图片镜像处理的方法

1
- (UIImage *)imageFlippedForRightToLeftLayoutDirection NS_AVAILABLE_IOS(9_0);

比如导航栏的返回键显示
1
2
3
4
5
6
7
8
9
10
- (UIImage *)rtl_imageFlippedForRightToLeftLayoutDirection {

if (kIsRTL) {
return [UIImage imageWithCGImage:self.CGImage
scale:self.scale
orientation:UIImageOrientationUpMirrored];
}

return self;
}

定义一个宏方法,如果遇到需要反转的图片就使用这个方法
1
#define XGetImageWithRTL(imageName) [[UIImage imageNamed:[NSString stringWithFormat:@"%@",imageName]] rtl_imageFlippedForRightToLeftLayoutDirection]

也可以直接在 image Assets.xcassets 资源包中设置
image.png|500

UIButton

UIEdgeInsets 也会影响到布局,系统没有处理这个内容,手动处理
hook了UIButton的setContentEdgeInsets,setImageEdgeInsets,setTitleEdgeInsets方法在RTL情况下,手动调换left <-> right

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
UIEdgeInsets RTLEdgeInsetsWithInsets(UIEdgeInsets insets) {
if (insets.left != insets.right && kIsRTL) {
CGFloat temp = insets.left;
insets.left = insets.right;
insets.right = temp;
}
return insets;
}

@implementation UIButton (RTL)

+ (void)load {
RTLMethodSwizzling(self, @selector(setContentEdgeInsets:), @selector(rtl_setContentEdgeInsets:));
RTLMethodSwizzling(self, @selector(setImageEdgeInsets:), @selector(rtl_setImageEdgeInsets:));
RTLMethodSwizzling(self, @selector(setTitleEdgeInsets:), @selector(rtl_setTitleEdgeInsets:));
}

- (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

UIScrollView

分页控制器处理
将ScrollView旋转180度,上面的子视图再旋转180度

1
2
3
self.scrollView.transform = CGAffineTransformMakeRotation(M_PI);

subView.transform = CGAffineTransformMakeRotation(M_PI);

UICollectionView 水平方向

继承UICollectionViewFlowLayout 重写两个方法

1
2
3
4
5
6
7
8
9
10
11
-(UIUserInterfaceLayoutDirection)effectiveUserInterfaceLayoutDirection {

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

- (BOOL)flipsHorizontallyInOppositeLayoutDirection{
return YES;
}

总结

1.尽量使用AutoLayout处理布局,不要用left和right,改用 leading 和 trailing。
2.多语言遇到拼接字符串的话,要谨慎。其他LTR语言中,如果是拼接字符串,第一个字符是RTL语言,会出现显示问题
3.图片翻转也要谨慎使用

遇到问题

1

1
A constraint cannot be made between a leading/trailing attribute and a right/left attribute. Use leading/trailing for both or neither.

解决:
你在制定约束时,约束的对象和参考对象没有使用同一约束的成员属性。前者使用 leading,后者也要使用 leading