Ios布局相关知识梳理

iOS 布局机制

iOS 布局机制大概分这么几个层次:

  • frame layout
  • autoresizing
  • auto layout
frame layout

即通过设置view的frame/bounds属性值进而控制view相对于superview的位置和大小; 设置view的frame/bounds一般都是根据屏幕大小计算的,最常见的做法是将屏幕宽高定义为常量,还有就是创建UIView分类实现x,y,width,height等方法直接获取view的宽高等;

let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height
extension UIView {
    public var width:CGFloat {
        get {
            return self.frame.width
        }set {
            var rect = self.frame
            rect.size.width = newValue
            self.frame = rect
        }
    }
}
autoresizing

基于autoresizing机制,能够让subview和superview维持一定的布局关系,当superview 的size改变时,subview也会 做出相应的调整;一般用于各种屏幕的适配以及横竖屏的适配; 可以通过UIView的属性autoresizingMask实现autoresizing:

    public struct AutoresizingMask : OptionSet {
        public init(rawValue: UInt)

        // 自动调整view与父视图左边距,以保证右边距不变
        public static var flexibleLeftMargin: UIView.AutoresizingMask { get }
        // 自动调整view的宽度,保证左边距和右边距不变
        public static var flexibleWidth: UIView.AutoresizingMask { get }
        // 自动调整view与父视图右边距,以保证左边距不变
        public static var flexibleRightMargin: UIView.AutoresizingMask { get }
        // 自动调整view与父视图上边距,以保证下边距不变
        public static var flexibleTopMargin: UIView.AutoresizingMask { get }
        // 自动调整view的高度,以保证上边距和下边距不变
        public static var flexibleHeight: UIView.AutoresizingMask { get }
        // 自动调整view与父视图的下边距,以保证上边距不变
        public static var flexibleBottomMargin: UIView.AutoresizingMask { get }
    }

横竖屏适配的场景:

let view = UIView.init()
view.backgroundColor = UIColor.blue
view.frame = CGRect.init(x: 0, y: 0, width: kScreenWidth, height: 200)
view.autoresizingMask = .flexibleWidth
self.view.addSubview(view)

ddd

autoresizingMask也可以设置多种:

view.autoresizingMask = [.flexibleWidth,.flexibleBottomMargin]

作用是:view的宽度按照父视图的宽度比例进行缩放,距离父视图顶部距离不变

auto layout

基于 autoresizing 机制,我们只能处理subview和superview的关系,而无法处理兄弟view 之间的关系,也无法反向处理,如让superview依据subview的大小进行调整。而auto layout能很好的处理各种关系,它是一种基于约束的布局系统,可以根据元素上设置的约束自动调整元素的位置和大小。

autoresizing 和 auto layout 只能二选一,若要对某个view 采用auto layout布局,则需要设置其translatesAutoresizingMaskIntoConstraints属性值为false。

代码实现auto layout大致有三种方式:

  • UIKit框架提供的自动布局的NSLayoutConstraint方法
    /**
     设置约束
     @param view1 指定需要添加约束的视图一
     @param attr1 指定视图一需要约束的属性
     @param relation 指定视图一和视图二添加约束的关系
     @param view2 指定视图一依赖关系的视图二;attr1为height,width时可为nil
     @param attr2 指定视图一所依赖的视图二的属性,若view2=nil,该属性设置notAnAttribute
     @param multiplier 视图一相对视图二约束系数   
     @param c 视图一相对视图二约束偏移量 
     @return 返回生成的约束对象
     */
    public convenience init(item view1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation, toItem view2: Any?, attribute attr2: NSLayoutConstraint.Attribute, multiplier: CGFloat, constant c: CGFloat)
    
    let view = UIView.init()
    view.backgroundColor = UIColor.blue
    self.view.addSubview(view)
    view.translatesAutoresizingMaskIntoConstraints = false
    let constraint1 = NSLayoutConstraint.init(item: view, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 0)
    let constraint2 = NSLayoutConstraint.init(item: view, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0)
    let constraint3 = NSLayoutConstraint.init(item: view, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: 0)
    let constraint4 = NSLayoutConstraint.init(item: view, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 200)
    self.view.addConstraints([constraint1,constraint2,constraint3,constraint4])
    
  • VFL(Visual Format Language) VFL是一种声明性语言,VFL允许你通过一个格式化后的代码字符串迅速定义视图的自动布局约束。 VFL语法
    /**
     VFL设置约束
     @param format VFL语句,如:“H:|-0-[view]-0-|”
     @param opts 描述VFL中的所有对象的属性和布局的方向,默认directionLeadingToTrailing
     @param metrics VFL语句中使用到的变量,key值为VFL语句中的变量名
     @param views VFL语句中使用到的视图
     */
    open class func constraints(withVisualFormat format: String, options opts: NSLayoutConstraint.FormatOptions = [], metrics: [String : Any]?, views: [String : Any]) -> [NSLayoutConstraint]
    

    ``` let view = UIView.init() view.backgroundColor = UIColor.blue self.view.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false

// H:水平方向,V:垂直方向;|父视图, let constraint1 = NSLayoutConstraint.constraints(withVisualFormat: “H:|-0-[view]-0-|”, options: .directionLeadingToTrailing, metrics: nil, views: [“view”:view]) let constraint2 = NSLayoutConstraint.constraints(withVisualFormat: “V:|-0-[view(height)]”, options: .directionLeadingToTrailing, metrics: [“height” : 200], views: [“view”:view]) self.view.addConstraints(constraint1) self.view.addConstraints(constraint2)


- 第三方库 [SnapKit](https://github.com/SnapKit/SnapKit)

###### auto layout反向处理
auto layout能反向处理,如让superview依据subview的大小进行调整:

let superview = UIView.init() superview.backgroundColor = UIColor.orange self.view.addSubview(superview)

let subview1 = UIView.init() subview1.backgroundColor = UIColor.red superview.addSubview(subview1)

let subview2 = UIView.init() subview2.backgroundColor = UIColor.blue superview.addSubview(subview2)

superview.snp.makeConstraints { (make) in make.leading.equalTo(10) make.top.equalTo(20) make.width.equalTo(100) }

subview1.snp.makeConstraints { (make) in make.leading.top.equalToSuperview().offset(10) make.trailing.equalToSuperview().offset(-10) make.height.equalTo(100) }

subview2.snp.makeConstraints { (make) in make.leading.trailing.height.equalTo(subview1) make.top.equalTo(subview1.snp.bottom).offset(10) make.bottom.equalToSuperview().offset(-10) }

![mm](https://upload-images.jianshu.io/upload_images/2427856-742680b933e58b1b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

###### auto layout获取布局前size
在布局完成前,我们不能通过view.frame.size获取view的 size(这时size为{0,0})。有时候,我们需要在auto layout system对view完成布局前就知道它的size,例如UITableViewCell需要回调tableView:heightForRowAtIndexPath:返回每行的高度,这时可以使用`systemLayoutSizeFittingSize:`方法实现:

self.view.addSubview(label) label.snp.makeConstraints { (make) in make.leading.top.equalToSuperview().offset(20) } let size = label.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)


#### 自适应布局
自适应布局,即控件size由其content动态决定;具有content的控件如UILabel、UIButton等能直接计算出content(如UILabel的text、UIButton的title,UIImageView的image)的大小。
auto layout system在布局时,如果不知道该为view分配多大的size,就会回调view的`intrinsicContentSize`方法,该方法会给auto layout system一个合适的size,system根据此 size对view的大小进行设置。对于UILabel这类控件,intrinsicContentSize返回的是根据其content计算出的size。有些view不包含content,例如UIView,这种 view 被认为`has no intrinsic size`,它们的intrinsicContentSize返回的值是(-1,-1);
UIScrollView及其子类,虽然也包含 content,由于它们是滚动的,auto layout system在对这类view进行布局时总会存在一些未定因素,这些 view的intrinsicContentSize也直接返回(-1,-1);
因此,对于能返回正确的intrinsicContentSize的view,auto layout可以使view自适应:

let label = UILabel.init() label.backgroundColor = UIColor.orange label.translatesAutoresizingMaskIntoConstraints = false label.text = “chang a chang” self.view.addSubview(label) let constraint1 = NSLayoutConstraint.init(item: label, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 10) let constraint2 = NSLayoutConstraint.init(item: label, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 20) self.view.addConstraints([constraint1,constraint2])

![zz](https://upload-images.jianshu.io/upload_images/2427856-0dedb728d0d7ae52.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

对于多行文字的UILabel,需要指定Label的宽度才能自适应:
指定Label的宽度的两种方式:设置preferredMaxLayoutWidth 属性:

label.numberOfLines = 0 label.text = “chang a chang chang a chang chang a chang chang a chang chang a chang” self.view.addSubview(label) let constraint1 = NSLayoutConstraint.init(item: label, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 10) let constraint2 = NSLayoutConstraint.init(item: label, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 20) self.view.addConstraints([constraint1,constraint2]) label.preferredMaxLayoutWidth = 100

设置label宽度约束:

let constraint1 = NSLayoutConstraint.init(item: label, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 10) let constraint2 = NSLayoutConstraint.init(item: label, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 20) let constraint3 = NSLayoutConstraint.init(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100) self.view.addConstraints([constraint1,constraint2,constraint3])


![rrr](https://upload-images.jianshu.io/upload_images/2427856-3ca05308d9921df7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

对于frame layout的控件,可以调用`sizeToFit`或`sizeThatFits`方法做到自适应size:

// 单行 label.x = 10 label.y = 20 label.text = “chang a chang” label.sizeToFit() self.view.addSubview(label)

// 多行 label.numberOfLines = 0 label.text = “chang a chang chang a chang chang a chang chang a chang chang a chang” let size = label.sizeThatFits(CGSize(width: 100, height: 0)) label.frame = CGRect(origin: CGPoint(x: 10, y: 20), size: size) self.view.addSubview(label)


对于intrinsicContentSize不能返回正常size的view,同样也可以使用`sizeToFit`或`sizeThatFits`方法手动做到自适应:

let textview = UITextView.init() textview.backgroundColor = UIColor.orange textview.translatesAutoresizingMaskIntoConstraints = false textview.text = “chang a chang chang a chang chang a chang chang a chang” self.view.addSubview(textview) let size = textview.sizeThatFits(CGSize(width: 100, height: 0)) let constraint1 = NSLayoutConstraint.init(item: textview, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 10) let constraint2 = NSLayoutConstraint.init(item: textview, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 20) let constraint3 = NSLayoutConstraint.init(item: textview, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100) let constraint4 = NSLayoutConstraint.init(item: textview, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: size.height) self.view.addConstraints([constraint1,constraint2,constraint3,constraint4])


####setNeedsLayout和layoutIfNeeded
`setNeedsLayout `的作用就是标记了一个flag,表示该控件需要刷新布局,此方法记录调整的布局请求并立即返回,它不会强制立即更新,而是会等待下一个更新周期才进行刷新页面布局;调整视图的子视图的布局时,系统默认会在主线程调用此方法的;我们也可以手动调用这个方法,标记该控件是需要刷新的,这样可以将所有的布局更新合并到一个更新周期,适合用来优化性能。
`layoutIfNeeded `,调用该方法会立即更新所有标记了flag的控件布局;
setNeedsLayout和layoutIfNeeded一般都是配合使用的,控件先调用setNeedsLayout再调用layoutIfNeeded就能实现立即更新布局的需求;(在控件第一次显示之前,肯定是有flag的,所以不用setNeedsLayout直接调用layoutIfNeeded也会进行立即更新);
之前一节中的**auto layout获取布局前size**使用layoutIfNeeded方法同样能获取到size:

label.snp.makeConstraints { (make) in make.leading.top.equalToSuperview().offset(20) } self.view.setNeedsLayout() self.view.layoutIfNeeded() print(label.frame) // (20.000000000000007, 20.0, 112.66666666666667, 20.333333333333332)

在使用Autolayout时,动画的使用和以前也不同了,frame layout时我们是修改frame,现在我们可以通过修改约束, 然后在动画时调用layoutIfNeeded就行了:

override func touchesBegan(_ touches: Set, with event: UIEvent?) { self.label?.snp.remakeConstraints { (make) in make.leading.top.equalToSuperview().offset(20) make.height.equalTo(200) } UIView.animate(withDuration: 0.5) { self.view.layoutIfNeeded() } } ``` 参考: [深入理解 Auto Layout 第一弹](https://zhangbuhuai.com/post/auto-layout-part-1.html)

Written on March 24, 2019