Auto Layout leading and trailing attributes and RTL

When you use the Auto Layout leading and trailing attributes (not the right and left attributes), most of the user interface appears mirrored in right-to-left languages.

For Mac apps, most controls—such as segmented controls, progress indicators, and outline views—also appear flipped. In iOS apps, UIKit controls appear flipped when the app links against iOS 9 and later.

oQxn8o

以上内容引用 Apple 的官方文档:Supporting Right-to-Left Languages

Autolayout

从苹果官方可以看出,leading就是前面,left左边,这两个值在大部分国家其实是一样的。trailing和right跟这个一样就不说了。

但是在部分国家,leading跟right是一样的,也就是有部分地方,如阿拉伯等右边是前面,也就是leading是right,这时候才有区别。

由从左到右(LeftToRight)的布局习惯变为了从右向左(RightToLeft)的布局习惯,就是我们常说的RTL

现在我们来验证一下,首先我们正常创建一个工程,然后给一个label设置约束(这里使用masonry框架)。
gfbPgG

代码大概是这样的,那么出来的效果
AEKnVT

这个在我们认知里是没问题的,leading就是left,但是同样的代码,我们这个时候如果把手机语言切换成阿拉伯语言,再看看效果。
5DJNGf

看到效果,你肯定会很奇怪,明明设置的leading是前面距离20,为什么变成后面距离20了。这里就是因为不同地方的定义不一样,在阿拉伯地区,leading是指right,所以前面就变成右边了。

我们再来试试left的效果。

正常的是这样的
Y8IwQv

阿拉伯的是这样的
cOFgmj

所以如果我们的APP不适配到阿拉伯地区,那么使用left,right就行了。但是如果是要适配这些地区,最好还是使用leading,trailing,因为这些地方的前后跟我们是不一样的。

View 的 RTL

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的属性,当我们将其设置成UISemanticContentAttributeForceRightToLeft之后,UIView将强制变为RTL布局。当然在非RTL语言下,我们需要设置它为UISemanticContentAttributeForceLeftToRight,来适配系统是阿拉伯语,App是其他语言不需要RTL布局的情况。

让一个App适配RTL,我们需要给几乎所有的View都设置这个属性,这种情况下,首先想到的是hook UIView的DESIGNATED_INITIALIZER,在里面设置semanticContentAttribute。但是这种办法有坑,WKWebview虽然继承于UIView,但是它的setSemanticContentAttribute:会有问题,会导致Crash:

3Mftwy

这应该是系统的坑,为了绕开这个坑,我们发现使用[UIView appearance]来设置能达到差不多的效果:

[UIView appearance].semanticContentAttribute  = UISemanticContentAttributeForceRightToLeft;

使用[UIView appearance]设置后,大部分的View看上去正常了。除了搜索栏。使用[UIView appearance]设置后,搜索栏是不生效的。不过不用担心,我们只需要设置一下[UISearchBar appearance]即可。

[UISearchBar appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;

Frame, 兄弟们咱就别用了

对于frame布局,系统就没这么友好了,frame的布局需要我们自己去适配。 探究RTL的布局,实际上只是调整了frame.origin.x,y和size是不会变的。而且对于静态view,如果知道了父view的width,是可以直接算出字view RTL下的frame的,所以我们封了一个category,来满足大部分静态布局的情况

@implementation UIView (HTSRTL)

- (void)setRTLFrame:(CGRect)frame width:(CGFloat)width
{
    if (isRTL()) {
        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];
}

@end

对于已经完成frame布局的view,我们只需要在最后对view调用resetFrameToFitRTL,即可适配RTL。

整体上,frame适配RTL还是比autolayout麻烦很多。兄弟们咱就别用了。

Image

在RTL下,某些图片是需要镜像的,比如带箭头的返回按钮。正常情况下,箭头是朝左的,RTL下,箭头就需要镜像成朝右。系统对这种情况提供了一个镜像的方法:

// Creates a version of this image that, when assigned to a UIImageView’s image property, draws its underlying image contents horizontally mirrored when running under a right-to-left language. Affects the flipsForRightToLeftLayoutDirection property; does not affect the imageOrientation property.
- (UIImage *)imageFlippedForRightToLeftLayoutDirection NS_AVAILABLE_IOS(9_0);

这个方法并不好用。通过切换系统语言,来适配RTL应该是没问题的。但是在App内部切换语言,手动修改RTL布局,系统的这个方法就经常出现错误镜像的情况。我们自己写一个方法,来达到这个目的:

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

    return self;
}
@end

对于需要在RTL下镜像的图片,手动对image调用hts_imageFlippedForRightToLeftLayoutDirection

目前遇到的大概这么多,后续再更新。