图片来自:picography.co/ocean-splas…
本文作者:JDMin

开篇

当今大约有超过 22 个国家,6.6 亿人运用阿拉伯文字,使其成为仅次于拉丁文和中文的国际第三大书面言语。随着事务在海外扩展的逐步深化,App 适配阿拉伯语已经提上了日程。与咱们平常触摸较多的中英文差异最显着的是,阿拉伯语的书写和运用习气是从右到左的。虽然 iOS 本身已经有许多关于这种 RTL(Right-To-Left) 言语的处理,可是在咱们开发的时分,需求留意运用正确的标准去防止过错。一起每个事务和 App 都有各自的一些特别规划和其他特色,这些特色也会带来许多新的问题待处理。下面介绍下最近工程在适配RTL言语中遇到的问题和处理。

在介绍具体的各个问题场景前,咱们先对 RTL 言语与工程适配相关的一些主要特色做下介绍:

  • 文本。与中文、英文等 LTR(Left-To-Right) 言语最显着的不同是,RTL 言语在书写和阅读习气上是从右到左的。

  • 图标。图标要针对每个具体图标灵活处理。考虑到 RTL 言语的案牍和运用习气是从右到左,所以许多有明确方向性的图标需求改动下方向(举个比方来说,比方常用的箭头图标)。至于其他惯例性图标,则在 UI 中保持不变。

  • 数字。咱们日常触摸较多是阿拉伯数字,或许称作西阿拉伯数字。与之相对的,是东阿拉伯数字。不同的阿拉伯国家运用不同的阿拉伯数字,比方摩洛哥、阿尔及利亚常用西阿拉伯数字,而伊朗、阿富汗、巴基斯坦等国家则运用东阿拉伯数字。而像埃及、沙特阿拉伯等国家,则两种形式的阿拉伯数字都会运用。开发前需求承认清楚咱们需求供给服务的区域运用的是哪种阿拉伯数字,而且正确的处理和展现。

工程现状和特色

实践上,iOS 体系已经对RTL言语做了许多处理,而且供给了许多 API 方便上层做事务适配。不过在评论这些具体的问题之前,咱们需求先了解当时工程的现状和特色,并依此来挑选最适宜的处理计划。总结来说,工程当时的几个特色:

  1. 体量较大。工程发展到今日,代码量已经比较巨大。关于比较大的改动需求考虑改造本钱,以及是不是会对将来事务扩展落下什么危险。
  2. 工程有大量的布局代码,特别是比较早期的事务的布局代码,是运用frame layout手动布局的办法处理,没有运用AutoLayout。
  3. App支撑用户运用内设置言语。运用首次发动会挑选用户的体系言语作为默许言语,一起支撑用户在运用内切换言语。

至于布局办法、运用内设置言语对 RTL 适配的具体影响,咱们在后文具体介绍。

遇到的问题

运用内切换言语

当咱们在体系设置中将言语设置为阿拉伯语等 RTL 言语后,体系会主动将 App 的布局办法改为 RTL 布局。这儿就会遇到榜首个问题,咱们 App 内能够设置言语,当运用设置言语和体系言语的布局办法不一致时(比方运用内设置成阿拉伯语,体系设置成英语),咱们期望以运用内言语为准。这个时分,就无法再运用体系的默许处理。在 iOS9 今后,iOS 为UIView 开放了一个新的property

@property (nonatomic) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0));

经过semanticContentAttribute 能够在由开发者自界说一个 view 在 RTL 和 RTL 布局下是否做翻转处理。咱们需求依据运用内言语设置App里 View 的semanticContentAttribute ,防止运用体系的默许判别。这儿对一个个 View 做改动显然过于麻烦, 咱们的做法是在言语设置的时分,经过设置UIView.appearance().semanticContentAttribute依据对大局做处理。

if isRTLLanguage {
    UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
    UIView.appearance().semanticContentAttribute = .forceLeftToRight
}

关于有特别适配场景的 View (在RTL模式下也不翻转),能够在事务顶层自行设置相关 UI 元素实例的semanticContentAttribute

布局

现在咱们常用的布局办法一般有2种。一种是运用 AutoLayout ,一种是frame layout的手动布局。咱们逐一介绍。

关于 AutoLayout ,包括常用的三方封装库 Masonry 、SnapKit 等,对 RTL 都已经有比较好的兼容处理。在 RTL 和 LTR 中,Left 和 Right 对应的实践方向相同,布局不会有改变。因而,咱们在设置束缚的时分,需求运用具有通用含义的 Leading 和 Trailing 来替换以往常用的 Left 和 Right 。Leading 为前部束缚,对应 LTR 中的 Left 和 RTL中的 Right。 Trailing 为尾部束缚,对应LTR中的 right 和 RTL 中的 Left。 运用 Leading 和 Trailing 设置束缚,View会依据本身的semanticContentAttribute 具体是 LTR 或是 RTL 主动调整布局。

因为咱们事务内当时有大量的布局代码是运用frame layout手动布局,全部切换到 AutoLayout 不现实。特别是关于有复杂 UI 元素和布局逻辑的场景,重写布局困难而且适当耗时。因而需求考虑给这种布局办法供给更小改动本钱的 RTL 适配办法。当咱们在 LTR 中设置left(view.origin.x) = a,映射到 RTL 坐标系,其实便是设置right(view.left + view.width) = view.superview.width - a。当咱们在 LTR 中设置right = a,映射到 RTL 坐标系,其实便是设置view.superview.width - a + self.width,因而,咱们能够将 2 个坐标体系一化,参照 AutoLayout 中的界说,扩展 View 的 leading 和 trailing 特色。

@implementation UIView (RTL)
- (CGFloat)leading {
    NSAssert(self.superview != nil, @"运用leading有必要当时view增加到superView!");
    if ([self isRTL]) {
        return self.superview.width - self.right;
    }
    return self.left;
}
- (void)setLeading:(CGFloat)leading {
    NSAssert(self.superview != nil, @"运用leading有必要当时view增加到superView!");
    if ([self isRTL]) {
        self.right = self.superview.width - leading;
    } else {
        self.left = leading;
    }
}
- (CGFloat)trailing {
    NSAssert(self.superview != nil, @"运用trailing有必要当时view增加到superView!");
    if ([self isRTL]) {
        return self.leading + self.width;
    }
    return self.right;
}
- (void)setTrailing:(CGFloat)trailing {
    NSAssert(self.superview != nil, @"运用trailing有必要当时view增加到superView!");
    if ([self isRTL]) {
        self.right = self.superview.width - trailing + self.width;
    } else {
        self.left = trailing - self.width;
    }
}
@end

在设置 leading 和 trailing 前,要求 View 已经增加到 superview ,而且 size 已经设置。这个在绝大多数场景下能够满意(以咱们工程为例,暂时没有遇到无法满意条件的场景)。 新增了这几个相关办法后,在RTL适配时,关于本来( LTR 场景)的 left 设置,改成运用 leading 设置。本来的 right 设置, 改成运用 trailing 设置。和AutoLayout的概念用法基本相同,适配本钱大幅减小。

Image

就像在上文说过,并不是一切图片都需求在 RTL 模式下翻转,只要一部分图片(一般来说,常常是有比较明确方向含义和性质图片)需求翻转。 关于需求翻转的图片,有几种办法能够处理。 在 iOS9 之后,UIImage 新增了相关办法,

- (UIImage *)imageFlippedForRightToLeftLayoutDirection API_AVAILABLE(ios(9.0));
@property (nonatomic, readonly) BOOL flipsForRightToLeftLayoutDirection API_AVAILABLE(ios(9.0));

关于需求在 LTR 和 RTL 下不同翻转的 image ,能够经过imageView.image = targetImage.imageFlippedForRightToLeftLayoutDirection()来设置。或许在 Image Set 中,设置相关图片资源的 Direction ,

项目RTL语言适配实践中遇到的问题和总结
需求留意的是,这两种办法是作用于UIImageView 上,关于其他容器会无效。一起要留意展现时是运用UIImageView semanticContentAttribute 做翻转判别,semanticContentAttribute 设置过错的话终究展现图片也会过错。 鉴于以上原因,能够在对UIImage 供给自界说的翻转办法,

@implementation UIImage (RTL)
- (UIImage *_Nonnull)checkOverturn {
    if (isRTL) {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        CGContextRef bitmap = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(bitmap, self.size.width / 2, self.size.height / 2);
        CGContextScaleCTM(bitmap, -1.0, -1.0);
        CGContextTranslateCTM(bitmap, -self.size.width / 2, -self.size.height / 2);
        CGContextDrawImage(bitmap, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage);
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        return image;
    }
    return self;
}
@end

一起供给对 View 容器的翻转办法,

@implementation UIView (RTL)
- (void)checkOverturn {
    // 防止重复翻转
    if (self.overturned) {
        return;
    }
    // 基于transform翻转
    self.transform = CGAffineTransformScale(self.transform, -1, 1);
}
@end

顶层事务能够依据实践场景,挑选适宜的办法处理。

文本

文本这儿需求处理的比较重要的问题有 3 个,一个是 text 的对齐办法( alignment )。一个是 AttributeString 的处理。一个是 text 里字符的摆放顺序(字符从左往右或许从右往左)。咱们逐一介绍。

alignment

咱们先评论 alignment。在NSText中,NSTextAlignment界说为,

/* Values for NSTextAlignment */
typedef NS_ENUM(NSInteger, NSTextAlignment) {
    NSTextAlignmentLeft      = 0,    // Visually left aligned
#if TARGET_ABI_USES_IOS_VALUES
    NSTextAlignmentCenter    = 1,    // Visually centered
    NSTextAlignmentRight     = 2,    // Visually right aligned
#else /* !TARGET_ABI_USES_IOS_VALUES */
    NSTextAlignmentRight     = 1,    // Visually right aligned
    NSTextAlignmentCenter    = 2,    // Visually centered
#endif
    NSTextAlignmentJustified = 3,    // Fully-justified. The last line in a paragraph is natural-aligned.
    NSTextAlignmentNatural   = 4     // Indicates the default alignment for script
} 

咱们以最常用的Text容器UILabel 为例。关于UILabel ,假如没有设置textAlignment ,在 iOS9 之前会默许是NSTextAlignmentLeft ,在 iOS9 之后默许是NSTextAlignmentNatural NSTextAlignmentNatural 会依据体系言语是否是 RTL ,主动帮咱们调整适宜的 alignment 。关于需求运用内设置言语的场景,因为运用内言语可能和体系言语不一致,没法运用体系的默许处理。需求依据当时运用内是否设置是RTL言语,手动设置UILabel textAlignment 。出于快捷性考虑,能够扩展UILabelrtlAlignment办法。事务层依据需求设置rtlAlignment

typedef NS_ENUM(NSUInteger, NMLLabelRTLAlignment) {
    NMLLabelRTLAlignmentUndefine,
    NMLLabelRTLAlignmentLeft,
    NMLLabelRTLAlignmentRight,
    NMLLabelRTLAlignmentCenter,
};
@implementation UILabel (RTL)
- (void)setRtlAlignment:(RTLAlignment)rtlAlignment {
    [self bk_associateValue:@(rtlAlignment) withKey:@selector(rtlAlignment)];
    switch (rtlAlignment) {
        case RTLAlignmentLeading:
            self.textAlignment = (isRTL ? NSTextAlignmentRight : NSTextAlignmentLeft);
            break;
        case RTLAlignmentTrailing:
            self.textAlignment = (isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight);
            break;
        case RTLAlignmentCenter:
            self.textAlignment = NSTextAlignmentCenter;
        case RTLAlignmentUndefine:
            break;
        default:
            break;
    }
}
- (RTLAlignment)rtlAlignment {
    NSNumber *identifier = [self bk_associatedValueForKey:@selector(rtlAlignment)];
    if (identifier) {
        return identifier.integerValue;
    }
    return RTLAlignmentUndefine;
}
@end

AttributeString 的处理

因为设置 textAlignment 无法对 AttributeString 收效,所以 AttributeString 需求单独处理。处理办法和设置 textAlignment 相似,只是换成运用NSParagraphStyle 来处理。

@implementation NSMutableAttributedString (RTL)
- (void)setRtlAlignment:(RTLAlignment)rtlAlignment {
    switch (rtlAlignment) {
        case RTLAlignmentLeading:
            self.yy_alignment = (isRTL ? NSTextAlignmentRight : NSTextAlignmentLeft);
            break;
        case RTLAlignmentTrailing:
            self.yy_alignment = (isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight);
            break;
        case RTLAlignmentCenter:
            self.yy_alignment = NSTextAlignmentCenter;
        case RTLAlignmentUndefine:
            break;
        default:
            break;
    }
}
@end

字符摆放顺序

体系会运用 Text 的榜首个字符作为摆放顺序的判别依据。比方文本” 你好”,因为榜首个字符是阿拉伯语字符,所以体系会运用 RTL 规矩处理。同理,假如文本是”你好 “,因为榜首个字符是中文,则会运用 LTR 规矩。这个处理办法在 Text 中只要单一言语时没有问题,不过遇到 RTL 言语和 LTR 言语混合的场景,状况就会变得复杂许多,需求有更细致的考虑。

以一个常见的比方阐明,比方谈天消息中经常运用的@格局语法,在 LTR 和 RTL 中大概有这些场景,

项目RTL语言适配实践中遇到的问题和总结

能够看到,虽然体系的这个默许处理能够应对多数的状况。可是在一些场景下无法满意需求,比方上面的label[3]。 咱们期望将”@”与后边的用户称号视为一个整体,关于 “@我, 今日天气好吗”,咱们预期展现成 “@我,今日天气好吗”,可是终究展现成了”我, 今日天气好吗@”。或许比方咱们期望是以 LTR 展现,

项目RTL语言适配实践中遇到的问题和总结
可是终究会展现成,
项目RTL语言适配实践中遇到的问题和总结

关于这些场景,咱们需求刺进一些相关的 Unicode 来做纠正。比较常用的相关的 Unicode 有以下这些。

项目RTL语言适配实践中遇到的问题和总结

再回到刚才 2 个比方,关于”@我, 今日天气好吗”,iOS 将@也当成了阿拉伯语的一部分,咱们需求对@手动增加 LEFT-TO-RIGHT 标志 \u200E,声明为LTR展现。关于第2个比方,咱们需求对几个阿拉伯文增加\u202A声明为LTR展现,一起运用\u202C作为完毕标签。

项目RTL语言适配实践中遇到的问题和总结

其他留意点

除了以上介绍的这些,还有一些比较零星的点需求留意。

UICollectionView

UICollectionView 在 RTL 场景下也需求翻转,体系不会帮咱们默许做这个事情,需求咱们自行处理。在 iOS11 之后,UICollectionViewLayout扩展了一个readonly property

@property(nonatomic, readonly) BOOL flipsHorizontallyInOppositeLayoutDirection;

flipsHorizontallyInOppositeLayoutDirection默许为false,当设置为true时,UICollectionView 会依据当时 RTL 状况,翻转水平坐标系。因为这是一个readonly的特色,咱们需求继承UICollectionViewLayout 并改写flipsHorizontallyInOppositeLayoutDirection的 getter 办法。

UIEdgeInsets

UIEdgeInsets 中界说的是leftright,在 RTL 场景下,体系不会帮咱们做翻转处理。虽然在 iOS11 今后,体系新增了NSDirectionalEdgeInsets界说,可是对常用的 UI 控件(比方UIButton 等)并没有扩展相关特色,仍是需求设置UIEdgeInsets。因而能够考虑新增相似UIEdgeInsetsMake_RTLFlip的界说,方便上层运用。

UIEdgeInsets UIEdgeInsetsMake_RTLFlip(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right)
{
    if (!isRTL)
    {
        UIEdgeInsets insets = {top, left, bottom, right};
        return insets;
    }
    UIEdgeInsets insets = {top, right, bottom, left};
    return insets;
}

UINavigationController

navigationBar 的滑动回来手势,会依据当时体系言语做 RTL 处理。关于咱们常用的 LTR 场景,是右滑回来。在 RTL 场景下是左滑回来。关于运用内自界说言语的场景,设置UIView.appearance().semanticContentAttribute不会改动这个手势,还需求设置UINavigationController.view.semanticContentAttribute

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        self.view.semanticContentAttribute = [UIView appearance].semanticContentAttribute;
    }
    return self;
}

Gesture

关于带方向性的Gesture(比方UISwipeGestureRecognizer等),体系不会对手势的响应方向做改动。这个只能上层依据当时是否是 RTL 场景做逻辑判别。一般来说,这类手势并不会非常频繁的运用,因而事务层适配处理本钱不大。

数字

如同开篇时介绍的那样,数字同样是需求考虑的一个重要的点。究竟是运用西方阿拉伯数字,仍是东方阿拉伯数字,在数字规矩和展现上都有差异。因为这次事务适配运用的西方阿拉伯数字规矩,和咱们日常触摸的相同,这儿就不再展开。假如是运用东阿拉伯数字,那数字逻辑就要额定处理。

总结

到这儿,整体的 RTL 兼容基本完成。总结来说,因为当时App需求支撑运用内设置言语,导致不少问题变得复杂化。而且因为App本身的许多特色,在计划规划的时分需求挑选改动本钱和风险都相对可控的计划来处理。关于不需求运用内独立设置App言语,或许是刚要从0到1开发App的话,能够依据本身的事务特色,规划更适宜当时事务的计划。

参考资料

  • Internationalization and Localization Guide
  • Design for Arabic
  • How to use Unicode controls for bidi text

本文发布自网易音乐技术团队,文章未经授权禁止任何形式的转载。咱们常年接收各类技术岗位,假如你准备换作业,又恰好喜爱云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。