项目要适配阿拉伯地区,而阿拉伯的言语是从右往左显示的,恰好与咱们的习惯相反,适配起来很别扭。

RTL布局(Right To Left)

咱们这边的习惯是从左到右,设计图也是如此:

【iOS】分享几个用于「绝对布局」适配RTL布局的Extension

而阿拉伯地区的习惯是从右到左的:

【iOS】分享几个用于「绝对布局」适配RTL布局的Extension
  • 除了字符和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_refWidthbounds.width,要设置contentSize.width
  • UICollectionView会主动适配,不过contentOffsetcontentInset依旧需要进行转化。
  • 另外这个参照宽度最好是不会变动的,假如变动了记住rtl_refWidth也更新一下。

目前适配的这几个类和属性就够用了(今后发现新的再补上),这里是我用纯frame布局适配建立好的UI:

【iOS】分享几个用于「绝对布局」适配RTL布局的Extension
【iOS】分享几个用于「绝对布局」适配RTL布局的Extension

全程都是依照从左到右的习惯建立的UI,没毛病。

最后说两句

当然能运用AutoLayout能省去很多费事,不过关于动态性比较强的界面,或者一些暂时穿插的控件,frame布局比AutoLayout好用,还有动画、交互强的地方,用frame能够很好地去操控,而且性能也比AutoLayout好一点。

至于frame运用费事,其实只要编写标准,用起来也是很便利的,所以我个人是挺喜爱frame布局的。