【iOS】分享几个用于「绝对布局」适配RTL布局的分类
项目要适配阿拉伯地区,而阿拉伯的语言是从右往左显示的,恰好与我们的习惯相反,适配起来很别扭。
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`。
var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superview?.bounds.width ?? 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_value(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
}
CALayer+RTL
import UIKit
private var refWidthKey: UInt8 = 0
extension CALayer {
/// 参照宽度,也就是父视图的宽度。
/// - 如果是`CAScrollLayer`最好将其设置为它的`内容宽度`。
var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superlayer?.bounds.width ?? 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
}
/// 相对自身的转换值
@objc func rtl_value(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
}
UIScrollView+RTL
import UIKit
extension UIScrollView {
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 = contentSize.width - bounds.width - newValue.x
contentOffset = CGPoint(x: offetX, y: newValue.y)
}
get {
guard isRTL else { return contentOffset }
let offetX = contentSize.width - 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 = contentSize.width - bounds.width - contentOffset.x
offset = CGPoint(x: offetX, y: contentOffset.y)
}
setContentOffset(offset, animated: animated)
}
/// 相对自身的转换值
override func rtl_value(_ v: CGFloat) -> CGFloat {
isRTL ? (contentSize.width - v) : v
}
}
使用
使用我这个分类的话,首先要给设置一个参照宽度(一般是父视图的宽度)
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也更新一下。
目前适配的这几个类和属性就够用了(以后发现新的再补上),这里是我用纯frame布局适配搭建好的UI:
全程都是按照从左到右的习惯搭建的UI,没毛病。
最后说两句
当然能使用AutoLayout能省去很多麻烦,不过对于动态性比较强的界面,或者一些临时穿插的控件,frame布局比AutoLayout好用,还有动画、交互强的地方,用frame可以很好地去控制,而且性能也比AutoLayout好一点。
至于frame使用麻烦,其实只要编写规范,用起来也是很方便的,所以我个人是挺喜欢frame布局的。

