项目RTL语言适配实践
因为我们业务涉及到中东地区,最近App开发过程中做了阿拉伯语言的适配.阿语与我们常见的英语最明显的区别是他们的书写习惯是从右向左的.下面介绍最近工程在适配RTL(Right-To-Left)上遇到的问题和解决方案:
应用内切换语言
当我们在系统设置中将语言设置中阿拉伯等RTL语言的时候,系统会自动将App的布局方式改成RTL布局方式.但是我们往往App内是可以设置语言的,当应用内语言设置和系统设置语言不一致的时候,我们应该以应用内语言为准.这时候就无法使用系统默认的处理方式了,这时候我们可以使用UIView的一个property,
open var semanticContentAttribute: UISemanticContentAttribute
通过 semanticContentAttribute,可以由我们开发者在一个自定义的View进行RTL的布局.我们需要根据应用内设置的语言来设置View的semanticContentAttribute.本人项目中定义了一个语言的单例类,在这里定义了一个isRTLLanguage的属性.做法:
if Language.share.isRTLLanguage {
UIView.appearance().semanticContentAttribute = .forceRightToLeft
self.navigationController?.view.semanticContentAttribute = .forceRightToLeft
} else {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
self.navigationController?.view.semanticContentAttribute = .forceLeftToRight
}
此代码我是放在项目View Controller的基类统一处理的,对于有特殊场景的View,可以在业务顶层自行设置.
布局
我们的布局有两种方式:
1、AutoLayout 自动布局.
对于AutoLayout,常见的第三方封装库有Masonry、SnapKit等,他们对于RTL已经有了比较好的兼容.在RTL和LTR中,left和right是对应的实际对应方向,布局不会有变化,因此我们设置约束的时候需要使用leading和trailing.leading在LTR中对应着布局中的Left,在RTL中对应着Right.Trailing对应着LTR的Right和RTL的Left.所以使用了Leading和Trailing后,View会根据自身的semanticContentAttribute的属性自动布局的.
注意: UIEdgeInsets
由于UIEdgeInsets中定义的是left和right,在RTL场景下,系统不会帮助我们做翻转处理.所以需要考虑UIEdgeInsets的翻转处理,具体处理方式就不累述了.
2、Frame 手动布局.
在一些较老或者页面动画的的代码使用的是frame layout的布局方式,为了方便更小成本的适配RTL.可以给View扩展leading和trailing属性.
extension UIView {
//MARK: RTL
@objc
var leading: CGFloat {
set {
assert(self.superview != nil, "使用leading必须当前view添加到superView!")
if Language.share.isRTLLanguage {
self.right = self.superview!.width - newValue
}else{
self.left = newValue
}
}
get {
assert(self.superview != nil, "使用leading必须当前view添加到superView!")
if Language.share.isRTLLanguage {
return self.superview!.width - self.right
}else{
return self.left
}
}
}
var trailing: CGFloat {
set {
assert(self.superview != nil, "使用trailing必须当前view添加到superView!")
if Language.share.isRTLLanguage {
self.right = self.superview!.width - newValue + self.width
}else{
self.left = newValue - self.width;
}
}
get {
assert(self.superview != nil, "使用trailing必须当前view添加到superView!")
if Language.share.isRTLLanguage {
return self.leading + self.width;
}else{
return self.right
}
}
}
}
对于原本使用left和right的场景,改成leading和trailing.
Image
其实在项目中,并不是所有的图片都需要在RTL模式下翻转,只有一部分具有指向性的图片需要翻转,UIImage有相关的方法:
- (UIImage *)imageFlippedForRightToLeftLayoutDirection API_AVAILABLE(ios(9.0));
对于需要在LTR和RTL在需要不同翻转的image,可以通过UIImage(named: name)?.imageFlippedForRightToLeftLayoutDirection()来设置.
Tip:对于新项目,可以做一个初始化图片的封装方法:
if Language.share.isRTLLanguage {
if name == "me_item_jt" {
return UIImage(named: name)?.imageFlippedForRightToLeftLayoutDirection()
}
}
return UIImage(named: name)
}
这样可以创建需要镜像处理的图片白名单,直接在方法里处理掉,可以省去在翻找代码处理.
文本
1、text的对齐方式(alignment)
alignment
在NSText中,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
}
对于我们最常用的UILabel,如果没有设置textAlignment,在iOS9以后系统默认是NSTextAlignmentNaturallei类型. NSTextAlignmentNatural会根据系统语言,自动帮我们适配RTL,但是对于我们应用内设置语言的场景,我们需要手动设置UILabel的textAlignment.因为我系统中所有的UILabel初始化基本做了一层拓展方法处理,所以在这里全部适配一下:
extension UILabel {
convenience init(_ text:String, color:UIColor, font:UIFont, textAlignment:NSTextAlignment? = nil) {
self.init()
self.text = text
self.textColor = color
self.font = font
self.textAlignment = textAlignment ?? (Language.share.isRTLLanguage ? .right : .left)
}
convenience init(_ color:UIColor, font:UIFont, textAlignment:NSTextAlignment? = nil) {
self.init()
self.textColor = color
self.font = font
self.textAlignment = textAlignment ?? (Language.share.isRTLLanguage ? .right : .left)
}
}
其他一些顶层业务的处理,具体情况具体调整,但是整体还是省了不少的时间. 如果是新的项目,可以为UILabel扩展rtlAlignment方法,业务层根据需要设置rtlAligment.
extension UILabel {
var rtlAlignment: NSTextAlignment {
get {
return self.textAlignment
}
set {
if Language.share.isRTLLanguage {
switch newValue {
case .left:
self.textAlignment = .right
case .right:
self.textAlignment = .left
default:
self.textAlignment = newValue
}
}else {
self.textAlignment = newValue
}
}
}
}
2. AttributeString 的处理
由于设置 textAlignment 无法对 AttributeString 生效,所以 AttributeString 需要单独处理。处理方式和设置 textAlignment 类似,只是换成使用NSParagraphStyle 来处理。
3. 字符排列顺序
对于字符串,系统会根据string的第一个字符作为排序的依据.比如文本"مرحبا 你好",因为第一个字符是阿拉伯语字符,所以系统会使用 RTL 规则处理。同理,如果文本是"你好 مرحبا",因为第一个字符是中文,则会使用 LTR 规则。一般使用单一语言的时候,不会有什么问题,不过在RTL和LTR使用复杂的情况下,情况就会变的复杂,这时候我们可以使用Unicode来做相关的纠正.比较常用的相关的Unicode有下边这些.
以 \u{202C} 为例子,它是Unicode中的控制字符,称为 "POP DIRECTIONAL FORMATTING",它的作用是将文本方向恢复为先前的方向。它通常用于在双向文本中更改文本方向,以便在文本中插入 LTR(从左到右)文本或 RTL(从右到左)文本时,确保正确的文本方向。
iOS 将@也当成了阿拉伯语مرحبا的一部分,我们需要对@手动添加 LEFT-TO-RIGHT 标志 \u{202C},声明为LTR展示,OC相关的例子我这里就不列举了,具体可以查看其他相关的文章,这里我举一个项目中碰到的情况
//翻译
"or %@ interest-free payments of %@ with" = "%@ مدفوعات بدون فوائد من %@";
//代码
.lsFormat("or %@ interest-free payments of %@ with", 4,"\u{200E}\($1.0)")
.lsFormat("or %@ interest-free payments of %@ with", 4,"\($1.0)")
上边就是这种情况下的实现效果.
UICollectionView
UICollectionView在RTL场景下也需要翻转,系统不会帮我们做这件事,需要我们自己处理,在iOS11之后,给UICollectionView扩展一个属性
override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
UINavigationController
navigationBar 的滑动返回手势的处理
self.navigationController?.view.semanticContentAttribute = isRTLLanguage ? forceRightToLeft : .forceLeftToRight
我的项目中是和View整体一起处理掉了.
总结
总体的RTL的兼容基本完成.对于老项目App的一些特点,在适配的时候会有一些稍微复杂的情况出现,在方案设计的时候需要从改动成本和风险都相对可控为前提,对于一些从0到1开发的的App尤其涉及到海外的项目,可以根据自身业务特点,设计更加合理的解决方案.