自动布局那些事

2018-01-15  本文已影响0人  qiuyuliang

Auto Layout是苹果在iOS 6中引进的新技术,这是一种基于约束系统的布局规则,它的出现颠覆了开发人员创建界面的方式,同时我们也发现在较新版本的Android Studio中,很多通过模板创建的应用程序也默认采用了constraint-layout,可见基于约束规则来创建移动软件界面的方式已经被大家普遍认可。

Autoresizing系统

图1
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        /* 此处略去部分代码*/
        print("width:\(contentViewWidth) height:\(contentViewHeight)")
        //width:320.0 height:44.0
        
        let label = UILabel(frame: CGRect(x: contentViewWidth - lableWidth - margin, y: contentViewHeight - fontsize - margin, width: lableWidth, height: fontsize))
        label.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]
        contentView.addSubview(label)
}
override func layoutSubviews() {
        super.layoutSubviews()
        //label.frame = ...
}
let tableView = UITableView(frame: view.bounds, style: .plain)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

开始使用Auto Layout

场景

类似的场景还有很多,如果不使用Auto Layout来布局,你可能需要一些看似不太复杂的计算,而这些计算往往可读性很差,通常会定义若干个常量,而且很难适应各种屏幕。

Auto Layout

Intrinsic Content Size

视图内容的大小通过每个视图的intrinsicContentSize属性表达,它描述了数据未经压缩或裁剪的情况下表达视图全部内容所需的最小空间。

override var intrinsicContentSize: CGSize {
        return CGSize(width: 44, height: 44)
}

UILayoutFittingCompressedSize
@available(iOS 6.0, *)
public let UILayoutFittingCompressedSize: CGSize
图2

translatesAutoresizingMaskIntoConstraints : A Boolean value that determines whether the view’s autoresizing mask is translated into Auto Layout constraints.(是否要把autoresizing转成约束)

    //找回密码按钮
    findPwdButton.translatesAutoresizingMaskIntoConstraints = false
    //紧急冻结按钮
    freezeButton.translatesAutoresizingMaskIntoConstraints = false
    //更多选项按钮
    moreButton.translatesAutoresizingMaskIntoConstraints = false
    //两条分割线
    separatorLine1.translatesAutoresizingMaskIntoConstraints = false
    separatorLine2.translatesAutoresizingMaskIntoConstraints = false
    let viewsDictionary = ["findPwdButton" : findPwdButton, "freezeButton" : freezeButton, "moreButton" : moreButton, "separatorLine1" : separatorLine1, "separatorLine2" : separatorLine2]
    let metric = ["separatorHeight" : 16]
    let constraints = [
        NSLayoutConstraint.constraints(
            withVisualFormat: "H:|[findPwdButton]-[separatorLine1(1)]-[freezeButton]-[separatorLine2(1)]-[moreButton]|",
            options: [.alignAllTop, .alignAllBottom],
            metrics: nil,
            views: viewsDictionary
        ),
        NSLayoutConstraint.constraints(
            withVisualFormat: "V:|[separatorLine1(separatorHeight)]|",
            options: [],
            metrics: metric,
            views: viewsDictionary
        )
    ]
    NSLayoutConstraint.activate(constraints.flatMap{ $0 })
VisualFormatLanguage 分析
VisualFormatLanguage小结

最后我们选用了上述的intrinsicContentSize属性,同时返回UILayoutFittingCompressedSize,表明视图将根据容器内部的约束(距离上下左右四个方向的约束缺一不可),选用一个最小的size来刚好包括这几个控件。

override var intrinsicContentSize: CGSize {
        return UILayoutFittingCompressedSize
    }
override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        loginBottomView.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            loginBottomView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            loginBottomView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
        } else {
            NSLayoutConstraint(item: loginBottomView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
            view.addConstraint(NSLayoutConstraint(item: loginBottomView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -10))
        }
    }
NSLayoutAnchor分析
UINavigationBar的isTranslucent
UINavigationBar.appearance().setBackgroundImage(image, for: .default)
UINavigationBar.appearance().barTintColor = UIColor.white
self.edgesForExtendedLayout = []
override func viewDidLayoutSubviews() {
        topToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            topToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
            topToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            topToolbar.heightAnchor.constraint(equalToConstant: 44).isActive = true
            topToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        } else {
            // Fallback on earlier versions
            let viewsDictionary = ["topToolbar" : topToolbar, "topLayoutGuide" : topLayoutGuide ] as [String : Any]
            let constraints = [
                NSLayoutConstraint.constraints(
                    withVisualFormat: "V:[topLayoutGuide][topToolbar(44)]",
                    options: [],
                    metrics: nil,
                    views: viewsDictionary
                ),
                NSLayoutConstraint.constraints(
                    withVisualFormat: "H:|[topToolbar]|",
                    options: [],
                    metrics: nil,
                    views: viewsDictionary
                )
            ]
            NSLayoutConstraint.activate(constraints.flatMap{ $0 })
        }
    }

topLayoutGuide

safeAreaLayoutGuide

相信前面的内容已经让你对安全区域有了一定的了解,接下来我们来介绍它


图4.png

iPhone X底部控件适配

图5.png
    override func viewDidLayoutSubviews() {
        bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            //左侧紧贴父视图左侧
            bottomToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            
            //底部贴紧安全区域底部
            bottomToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            
            //右侧紧贴父视图右侧
            bottomToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            
            //高度为60
            bottomToolbar.heightAnchor.constraint(equalToConstant: 60).isActive = true
            
        } else {
            // Fallback on earlier versions
            NSLayoutConstraint(item: bottomToolbar, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 44).isActive = true
        }
    }

约束分析

override func layoutSubviews() {
        super.layoutSubviews()
        
        for view in subviews {
            if view .isKind(of: NSClassFromString("_UIToolbarContentView")!) {
                view.isUserInteractionEnabled = false
            }
        }
    }
图6.png
override func viewDidLayoutSubviews() {
        bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            //左侧紧贴父视图左侧
            bottomToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            
            //底部贴紧安全区域底部
//            bottomToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            //底部改为贴紧屏幕底部
            bottomToolbar.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            
            //右侧紧贴父视图右侧
            bottomToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            
            //去掉高度为60的约束
            //bottomToolbar.heightAnchor.constraint(equalToConstant: 60).isActive = true
            //改为顶部距离安全区域底部为60
            bottomToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60).isActive = true
            
        } else {
            // Fallback on earlier versions
            
        }
    }
图7.png
override func layoutSubviews() {
        super.layoutSubviews()
        
        
        button.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            button.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
            button.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -12).isActive = true
        } else {
            button.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
            button.rightAnchor.constraint(equalTo: rightAnchor, constant: -12).isActive = true
        }
        button.widthAnchor.constraint(equalToConstant: 120).isActive = true
        button.heightAnchor.constraint(equalToConstant: 40).isActive = true
    }

iPhone X简易聊天输入框的适配

图8.png
override func layoutSubviews() {
        super.layoutSubviews()
        
        for view in subviews {
            if view .isKind(of: NSClassFromString("_UIToolbarContentView")!) {
                view.isUserInteractionEnabled = false
            }
        }
        
        textField.translatesAutoresizingMaskIntoConstraints = false
        button.translatesAutoresizingMaskIntoConstraints = false
        
        let viewsDictionary = ["textField" : textField, "button" : button] as [String : Any]

        let constraints = [
            NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-[textField]-[button]-|",
                options: [.alignAllCenterY],
                metrics: nil,
                views: viewsDictionary
            )
        ]
        NSLayoutConstraint.activate(constraints.flatMap{ $0 })
        
        NSLayoutConstraint(item: textField, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 6).isActive = true
        NSLayoutConstraint(item: textField, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: -6).isActive = true

    }
图9.png 图10.png

内容吸附

内容吸附约束限制视图允许自身伸展和填充视图的程度。如果内容吸附优先级较高,则将视图的框架与内在内容相匹配。

textField.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .horizontal)
button.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)

压缩阻力

压缩阻力约束阻止视图剪切其内容。高压缩阻力优先级可确保显示出视图的完整内在内容。

图11.png
button.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 751), for: .horizontal)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
var hideConstraint: NSLayoutConstraint?
var showConstraint: NSLayoutConstraint?
//键盘消失时底部贴紧安全区域底部
hideConstraint = textInputBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
hideConstraint?.isActive = true
            
//键盘出现时底部贴紧屏幕底部,该约束并未激活
showConstraint = textInputBar.bottomAnchor.constraint(equalTo: view.bottomAnchor)
showConstraint?.isActive = false
    @objc func keyboardWillShow(_ notification: NSNotification) {
        let userInfo = notification.userInfo
        
        adjustTextFieldByKeyboardState(state: true, keyboardInfo: userInfo!)
    }
    
    @objc func keyboardWillHide(_ notification: NSNotification) {
        
        let userInfo = notification.userInfo
        adjustTextFieldByKeyboardState(state: false, keyboardInfo: userInfo!)
    }
    
    func adjustTextFieldByKeyboardState(state: Bool, keyboardInfo: [AnyHashable : Any]) {
        
        if state {
            let keyboardFrameVal = keyboardInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue
            
            let keyboardFrame = keyboardFrameVal.cgRectValue
            
            let height = keyboardFrame.size.height
            
            showConstraint?.constant = -height
            hideConstraint?.isActive = false
            showConstraint?.isActive = true
            
        } else {
            hideConstraint?.isActive = true
            showConstraint?.isActive = false
        }
        
        let animationDurationValue = keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber
        let animationDuration = animationDurationValue.doubleValue
        
        UIView.animate(withDuration: animationDuration) {
            self.view.layoutIfNeeded()
        }
    }
图12.png

UIStackView初见

图13.png
lazy var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
        stackView.spacing = 0
        return stackView
    }()
for _ in 1...5 {
            let bottomTabBarItemView = BottomTabBarItemView()
            stackView.addArrangedSubview(bottomTabBarItemView)
}

Demo下载

Demo下载

上一篇 下一篇

猜你喜欢

热点阅读