项目要适配阿拉伯地区,而阿拉伯语是从右往左显示的,恰好与我们的习惯相反,适配起来很别扭。
RTL布局(Right To Left)
我们这边的习惯是从左到右,设计图也是如此:
而阿拉伯地区的习惯是从右到左的:
- 除了字符和UI布局,还有侧返手势也要做同样的处理。
针对这两种布局方式,如果使用自动布局AutoLayout的话就很轻松,只要把left换成leading,把right换成trailing就可以了。
但绝对布局frame就不行,毕竟有名字给你叫的:绝对不妥协,坐标点在哪就在哪。对于喜欢用绝对布局的开发者(例如我)就很不友好了。
为了frame布局也能适配RTL布局,专门写了这几个Extension用来平时开发使用:
首先设置一个全局变量,用于判断当前是否RTL(从右到左)布局
let isRTL: Bool = {
guard let window = UIApplication.shared.delegate?.window ?? nil else { return false }
let layoutDirection = UIView.userInterfaceLayoutDirection(for: window.semanticContentAttribute)
return layoutDirection == .rightToLeft
}()
UIView+RTL
import UIKit
private var refWidthKey: UInt8 = 0
extension UIView {
/// 参照宽度,也就是【父视图】的宽度。
/// - 如果【父视图】是`UIScrollView`最好将其设置为它的`contentSize.width`。
@objc var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superview?.bounds.maxX ?? 0 }
}
var rtl_frame: CGRect {
set {
guard isRTL else {
frame = newValue
return
}
let x = rtl_refWidth - newValue.maxX
frame = CGRect(origin: CGPoint(x: x, y: newValue.origin.y), size: newValue.size)
}
get {
guard isRTL else {
return frame
}
let x = rtl_refWidth - frame.maxX
return CGRect(origin: CGPoint(x: x, y: frame.origin.y), size: frame.size)
}
}
var rtl_center: CGPoint {
set {
guard isRTL else {
center = newValue
return
}
let centerX = rtl_refWidth - newValue.x
center = CGPoint(x: centerX, y: newValue.y)
}
get {
guard isRTL else {
return center
}
let centerX = rtl_refWidth - center.x
return CGPoint(x: centerX, y: center.y)
}
}
var rtl_x: CGFloat {
set {
guard isRTL else {
frame.origin.x = newValue
return
}
let x = rtl_refWidth - frame.width - newValue
frame.origin.x = x
}
get {
guard isRTL else {
return frame.origin.x
}
let x = rtl_refWidth - frame.maxX
return x
}
}
var rtl_midX: CGFloat {
guard isRTL else {
return frame.midX
}
let midX = rtl_refWidth - frame.midX
return midX
}
var rtl_maxX: CGFloat {
guard isRTL else {
return frame.maxX
}
return rtl_refWidth - frame.origin.x
}
/// 相对【自身宽度】的转换值
@objc func rtl_valueFromSelf(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
/// 相对【参照宽度】的转换值
func rtl_valueFromRef(_ v: CGFloat) -> CGFloat {
isRTL ? (rtl_refWidth - v) : v
}
}
extension UIView {
/// 沿着Y轴180°翻转(水平镜像)
func rtl_flip() {
guard isRTL else { return }
layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 1, 0)
}
}
CALayer+RTL
import UIKit
private var refWidthKey: UInt8 = 0
extension CALayer {
/// 参照宽度,也就是【父图层】的宽度。
/// - 如果【父图层】是`CAScrollLayer`最好将其设置为它的`内容宽度`。
@objc var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superlayer?.bounds.maxX ?? 0 }
}
var rtl_frame: CGRect {
set {
guard isRTL else {
frame = newValue
return
}
let x = rtl_refWidth - newValue.maxX
frame = CGRect(origin: CGPoint(x: x, y: newValue.origin.y), size: newValue.size)
}
get {
guard isRTL else {
return frame
}
let x = rtl_refWidth - frame.maxX
return CGRect(origin: CGPoint(x: x, y: frame.origin.y), size: frame.size)
}
}
var rtl_position: CGPoint {
set {
guard isRTL else {
position = newValue
return
}
let positionX = rtl_refWidth - newValue.x
position = CGPoint(x: positionX, y: newValue.y)
}
get {
guard isRTL else {
return position
}
let positionX = rtl_refWidth - position.x
return CGPoint(x: positionX, y: position.y)
}
}
var rtl_x: CGFloat {
set {
guard isRTL else {
frame.origin.x = newValue
return
}
let x = rtl_refWidth - frame.width - newValue
frame.origin.x = x
}
get {
guard isRTL else {
return frame.origin.x
}
let x = rtl_refWidth - frame.maxX
return x
}
}
var rtl_midX: CGFloat {
guard isRTL else {
return frame.midX
}
let midX = rtl_refWidth - frame.midX
return midX
}
var rtl_maxX: CGFloat {
guard isRTL else {
return frame.maxX
}
return rtl_refWidth - frame.origin.x
}
/// 相对【自身宽度】的转换值
func rtl_valueFromSelf(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
/// 相对【参照宽度】的转换值
func rtl_valueFromRef(_ v: CGFloat) -> CGFloat {
isRTL ? (rtl_refWidth - v) : v
}
}
extension CALayer {
/// 沿着Y轴180°翻转(水平镜像)
func rtl_flip() {
guard isRTL else { return }
transform = CATransform3DMakeRotation(CGFloat.pi, 0, 1, 0)
}
}
UIScrollView+RTL
import UIKit
private var contentRefWidthKey: UInt8 = 0
extension UIScrollView {
/// 内容参照宽度,也就是内容的总宽度:`contentSize.width`。
/// - `UIScrollView`的子视图、偏移量的参照宽度就是`contentSize.width`。
/// - 由于`UICollectionView`的`contentSize`设置后依旧会发生变动(不受控),
/// - 所以如果能提前知道总宽度就最好提前设置给`rtl_contentRefWidth`,不依赖`contentSize.width`。
@objc var rtl_contentRefWidth: CGFloat {
set { objc_setAssociatedObject(self, &contentRefWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &contentRefWidthKey) as? CGFloat ?? contentSize.width }
}
var rtl_contentInset: UIEdgeInsets {
set {
guard isRTL else {
contentInset = newValue
return
}
contentInset = UIEdgeInsets(top: newValue.top,
left: newValue.right,
bottom: newValue.bottom,
right: newValue.left)
}
get {
guard isRTL else {
return contentInset
}
return UIEdgeInsets(top: contentInset.top,
left: contentInset.right,
bottom: contentInset.bottom,
right: contentInset.left)
}
}
var rtl_contentOffset: CGPoint {
set {
guard isRTL else {
contentOffset = newValue
return
}
let offetX = rtl_contentRefWidth - bounds.width - newValue.x
contentOffset = CGPoint(x: offetX, y: newValue.y)
}
get {
guard isRTL else {
return contentOffset
}
let offetX = rtl_contentRefWidth - bounds.width - contentOffset.x
return CGPoint(x: offetX, y: contentOffset.y)
}
}
func rtl_setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
var offset = contentOffset
if isRTL {
let offetX = rtl_contentRefWidth - bounds.width - contentOffset.x
offset = CGPoint(x: offetX, y: contentOffset.y)
}
setContentOffset(offset, animated: animated)
}
/// 相对【自身宽度】的转换值(自身宽度在`ScrollView`中是内容参照宽度,也就是内容的总宽度)
override func rtl_valueFromSelf(_ v: CGFloat) -> CGFloat {
isRTL ? (rtl_contentRefWidth - v) : v
}
/// 相对自身的转换偏移量
func rtl_contentOffset(_ offset: CGPoint) -> CGPoint {
guard isRTL else {
return offset
}
let offetX = rtl_contentRefWidth - bounds.width - offset.x
return CGPoint(x: offetX, y: offset.y)
}
}
UIImage+RTL
import UIKit
extension UIImage {
var rtl: UIImage {
guard isRTL, !flipsForRightToLeftLayoutDirection else {
return self
}
return imageFlippedForRightToLeftLayoutDirection()
}
}
UILabel+RTL
extension UILabel {
/// 文本对齐方向
var rtl_alignment: NSTextAlignment {
set {
switch newValue {
case .left:
textAlignment = isRTL ? .right : .left
case .right:
textAlignment = isRTL ? .left : .right
default:
textAlignment = newValue
}
}
get {
switch textAlignment {
case .left:
return isRTL ? .right : textAlignment
case .right:
return isRTL ? .left : textAlignment
default:
return textAlignment
}
}
}
}
CAGradientLayer+RTL
extension CAGradientLayer {
/// 渐变的起点和终点
func rtl_set(startPoint: CGPoint, endPoint: CGPoint) {
guard isRTL else {
self.startPoint = startPoint
self.endPoint = endPoint
return
}
self.startPoint = CGPoint(x: endPoint.x, y: startPoint.y)
self.endPoint = CGPoint(x: startPoint.x, y: endPoint.y)
}
}
UICollectionViewFlowLayout+RTL
系统的UICollectionViewFlowLayout有自适应的RTL布局,可以自定义子类重写flipsHorizontallyInOppositeLayoutDirection返回true即可:
class RTLFlowLayout: UICollectionViewFlowLayout {
override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return isRTL
}
}
使用
使用我这个分类的话,首先要给设置一个参照宽度(一般是父视图的宽度)
let testView = UIView()
// 1.一定要先设置参照宽度(一般是父视图的宽度)
testView.rtl_refWidth = UIScreen.main.bounds.width
// 2.再使用rtl_frame代替frame设置布局
testView.rtl_frame = CGRect(x: 20, y: 50, width: 100, height: 100)
addSubview(testView)
RTL布局主要是针对x轴的布局做镜像处理,所以要有个参照宽度(一般是父视图的宽度)才能做x轴的镜像换算。
📢 注意:
- 如果没有设置
rtl_refWidth默认会取父视图的宽度,所以建议先添加到父视图再设置rtl_frame。 - 如果父视图是
UIScrollView,不能设置rtl_refWidth为bounds.width,要设置contentSize.width。 UICollectionView会自动适配,不过contentOffset和contentInset依旧需要进行转换。- 另外这个参照宽度最好是不会变动的。如果变动了,记得把
rtl_refWidth和rtl_contentRefWidth也更新一下!
目前适配的这几个类和属性就够用了(以后发现新的再补上),这里是我用纯frame布局适配搭建好的UI:
全程都是按照从左到右的习惯搭建的UI,没毛病。
最后说两句
当然能使用AutoLayout能省去很多麻烦,不过对于动态性比较强的界面,或者一些临时穿插的控件,frame布局比AutoLayout好用,还有动画、交互强的地方,用frame可以很好地去控制,而且性能也比AutoLayout好一点。
例如,使用frame布局可以很好地进行视图的动态构建(看到才创建),并且之后新创建的视图也可以实现比较好的过渡效果:
基于这点,可以更好地去构建精美的页面初始化:

- 多个请求同时发起,返回时则按顺序逐个创建新视图并展示
- 根据返回的数据,有需要才去创建/展示对应的视图
- 提升「复杂页面的初始化速度」和「性能优化」
至于frame使用麻烦,其实只要编写规范,用起来也是很方便的,见仁见智吧。总的来说,我个人是挺喜欢frame布局的。