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

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

RTL布局(Right To Left)

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

3091688657575_.pic.jpg

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

3101688657576_.pic.jpg
  • 除了字符和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:

3081688657575_.pic.jpg 3071688657574_.pic.jpg

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

最后说两句

当然能使用AutoLayout能省去很多麻烦,不过对于动态性比较强的界面,或者一些临时穿插的控件,frame布局比AutoLayout好用,还有动画、交互强的地方,用frame可以很好地去控制,而且性能也比AutoLayout好一点。

至于frame使用麻烦,其实只要编写规范,用起来也是很方便的,所以我个人是挺喜欢frame布局的。

全部评论

相关推荐

在打卡的大老虎很想潜...:你在找实习,没啥实习经历,技术栈放前面,项目多就分两页写,太紧凑了,项目你最多写两个,讲清楚就行,项目背景。用到的技术栈、亮点、难点如何解决,人工智能进面太难了,需求少。你可以加最新大模型的东西
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务