美文网首页iOS进阶iOS基础
iOS - UIStackView的使用

iOS - UIStackView的使用

作者: Longshihua | 来源:发表于2020-05-27 17:04 被阅读0次

    UIStackView

    iOS9中苹果在UIKit框架中引入了一个新的视图类UIStackViewUIStackView类提供了一个高效的接口用于平铺一行或一列的视图组合。stackView视图管理着所有在它的arrangedSubviews属性中的视图的布局。这些视图根据它们在 arrangedSubviews数组中的顺序沿着stackView视图的轴向排列。并可以根据当前屏幕大小和方向的变化动态调整它的内容,感觉起来就像是一个隐形的容器。实际上subviews的位置是根据设置的对齐、间距和大小属性来决定的。

    内部的原理是UIStackView类管理了Auto Layout约束。想象一下stackView其实就是一个基于 Auto Layout的抽象层从而使布局属性的创建简单化。你可以在一个主stackView中嵌套stackView从而让视图精确放置到相应的位置。

    注意:我们只负责定义UIStackView的位置(position),UIStackView的大小(size)是可选的。当没有设置size的时候,UIStackView会根据它的内容的大小来调整自己的大小,即子视图各个控件的大小决定了UIStackView的大小,当子视图的各个控件大小为0,那么UIStackView的大小同样0

    UIStackView的相关属性

    20171225115155596.png

    Storyboard中可以搜索UIStackView进行使用,可以看到包含水平和垂直两个StackView

    截屏2020-05-24 上午10.23.52.png

    Axis: 轴,定义stackView的方向,包括水平方向(Horizontal)【子视图水平排列】和垂直方向(vertical)【子视图垂直排列】,简单一点就是x或者y轴方向的确定

    20171225112929808.png

    Alignment: Alignment控制subView对齐方式

    垂直方向包括4种对齐方式

    20171225113006870.png

    水平方向包括6种对齐方式

    20171225113048502.png

    如果值是Fill则会调整子视图以适应空间变化,其他的值不会改变视图的大小。

    Fill:子视图填充StackView。
    Leading:靠左对齐。
    Trailing:靠右对齐。
    Center:子视图以中线为基准对齐。
    Top:靠顶部对齐。
    Bottom:靠底部对齐。
    First Baseline:按照第一个子视图中文字的第一行对齐。
    Last Baseline:按照最后一个子视图中文字的最后一行对齐。
    

    Distribution:填充形式,或者理解为定义subview的分布方式,简单一点理解就是宽或者高的排列情况

    20171225113129634.png

    Fill属性:

    沿Axis方向填充所有区域,为了实现这个功能,UIStackView会扩大其中的一个子视图填充额外的区域,该view有着最低的水平内容紧靠优先级(lowest horizontal content hugging priority),如果所有的view的优先级相等,那么会扩大第一个view

    截屏2020-05-27 下午3.36.45.png

    如果设置了spacing,那么这些 arrangedSubviews 之间的间距就是spacing。如果减去所有的spacing,所有的arrangedSubview的固有尺寸(intrinsicContentSize)不能填满或者超出stackView的尺寸,那还是会按照Hugging或者 CompressionResistance的优先级来拉伸或压缩一些 arrangedSubview。如果出现优先级相同的情况,就按排列顺序来拉伸或压缩。

    Equal Spacing属性:

    等间距排列,这种是使arrangedSubview之间的spacing相等,但是这个spacing是有可能大于stackView所设置的spacing,但是绝对不会小于。这个类型的布局可以这样理解,先按所有的 arrangedSubviewintrinsicContentSize布局,然后余下的空间均分为spacing,如果大于stackView设置的spacing那这样就OK了,如果小于就按照stackView设置的spacing,然后按照 CompressionResistance的优先级来压缩一个 arrangedSubview

    Fill Equally属性:

    这种就是stackView的尺寸减去所有的spacing之后均分给 arrangedSubviews,每个 arrangedSubview的尺寸是相同的。

    Equal Proportionally属性:

    这种跟FillEqually差不多,只不过这个不是将尺寸均分给arrangedSubviews,而是根据 arrangedSubviewsintrinsicContentSize按比例进行分配。

    Equal Centering属性:

    在布局方向上居中的 subviews等间距排列。这种是使 arrangedSubview的中心点之间的距离相等,这样每两个 arrangedSubview之间的spacing就有可能不是相等的,但是这个spacing仍然是大于等于 StackView设置的spacing的,不会是小于。这个类型布局仍然是如果 StackView有多余的空间会均分给 arrangedSubviews之间的spacing,如果空间不够那就按照 CompressionResistance的优先级压缩arrangedSubview

    Spacing 间距

    子控件之间的最小间距,之所以说是最小间距,因为stackView会根据一定的规则对内部空间布局,有的时候不能满足所有要求,比如:stackView本身宽度100,内部两个控件,宽度都为50,50+50+10就超过了本身宽度,这时会压缩其中一个子控件的宽度来满足最小间距。

    其他重要属性

    • subView和arrangedSubView

    开始使用Stack View前,我们先看一下它的属性subViewsarrangedSubvies属性的不同。如果想为stackView添加子视图,应该调用addArrangedSubview:insertArrangedSubview:atIndex:arrangedSubviews数组是subviews属性的子集等方法。

    要移除stackView中的subview,需要调用removeArrangedSubview:removeFromSuperview。移除arrangedSubview只是确保Stack View不再管理其约束,而非从视图层次结构中删除,理解这一点非常重要,而调用removeFromSuperview才是真正的去除。

    • UIStackView的Position和size

    虽然stackView允许我们不直接使用Auto Layout布局它的内容,但是我们仍然需要使用Auto Layout来布局它的位置。基本上至少需要两个,即x,y的布局来定义stackView显示的位置。如果没有额外的约束,那么系统会自动根据stackView的内容计算它显示的大小。

    1、根据stackView布局的方向决定计算大小的方式,简而言之,它的大小等于所有子控件在布局方向上的大小之和后加上子控件之间的间距。

    对于水平方向的stackView,各个子控件宽度相加+间距为stackView的总宽度,高度为子控件中高度最大的视图。

    对于垂直方向的stackView,它的宽度为子视图最大视图的宽度,高度为各个子视图的高度之和+子视图之间的间距。。

    我们也可以提供额外的约束来指定stackView的高、宽或者都指定。在指定之后,stackView会调节自己的布局和大小,让被管理的子视图填充对应的区域。准确的布局还得看stackView布局属性的设置。如:distribution,alignment等。

    • 动态的改变UIStackView的内容

    stackView会根据视图被添加、去除、插入到arrangedSubviews来动态更新布局,或者设置被管理视图的isHidden属性来控制布局,也可以更新布局方向(axis),这一点非常有用,能够很好的动态适配内容进行显示。

    下面看一个小功能,效果如下

    主要是简单的使用了一下UIStackView的嵌套,最底层是一个垂直stackView,然后为其添加相应的约束,并依次创建子视图添加到stackView,子视图包括两个label,一个水平的stackView,一个按钮。这里水平方向的stackView,该视图添加了3个按钮等间距排列,最好的Press Me按钮实现了点击隐藏和显示水平的stackView

    import UIKit
    import SnapKit
     
    class ViewController: UIViewController {
     
        var stackView: UIStackView = UIStackView()
        var nestedStackView = UIStackView()
     
        override func viewDidLoad() {
            super.viewDidLoad()
            stackView.translatesAutoresizingMaskIntoConstraints = false
            stackView.axis = .vertical //垂直方向布局
            stackView.alignment = .fill
            stackView.spacing = 20
            stackView.distribution = .fillEqually
            stackView.backgroundColor = UIColor.brown //注意:这里设置背景颜色无效,说明stackView并没有被渲染到屏幕上
            view.addSubview(stackView)
            stackView.snp.makeConstraints { (make) in
                make.left.right.top.bottom.equalToSuperview().inset(20)
            }
     
            var label = UILabel()
            label.text = "Label 1"
            label.backgroundColor = UIColor.red
            stackView.addArrangedSubview(label)
     
            label = UILabel()
            label.text = "Label 2"
            label.backgroundColor = UIColor.cyan
            stackView.addArrangedSubview(label)
     
            nestedStackView.axis = .horizontal // 水平方向布局
            nestedStackView.alignment = .fill
            nestedStackView.spacing = 20
            nestedStackView.distribution = .fillEqually
            nestedStackView.addArrangedSubview(UIButton(type: .infoDark))
            nestedStackView.addArrangedSubview(UIButton(type: .infoLight))
            nestedStackView.addArrangedSubview(UIButton(type: .contactAdd))
            stackView.addArrangedSubview(nestedStackView)
     
            // 添加按钮实现隐藏nestedStackView功能
            let button = UIButton(type: .custom)
            button.setTitle("Press Me", for: .normal)
            button.setTitleColor(UIColor.black, for: .normal)
            button.addTarget(self, action: #selector(pressMe(button:)), for: .touchUpInside)
            stackView.addArrangedSubview(button)
        }
     
        @objc func pressMe(button: UIButton) {
            UIView.animate(withDuration: 0.5) {
                self.nestedStackView.isHidden = !self.nestedStackView.isHidden
            }
        }
    }
    

    可以看到代码非常简单,并不需要像以前一样设置每一个视图的约束,使用stackView,只需要为最外层的stackView添加约束即可。

    Simulator Screen Shot - iPhone X - 2020-05-27 at 16.20.01.png

    实际应用场景

    首先看一下需要实现的效果图

    20180705091545861.png

    由图可知实现并不复杂,都是一些常见的UI控件,但是为了更容易布局,其实我们可以使用UIStackView,比如:上半部分的用户头像、用户名称、用户描述我们就可以使用一个UIStackView,减少不必要的布局约束,而且显示用户名称和大v也可以使用UIStackView,实现起来更加简单方便。这里简单看一下用户名称和大v图片的显示实现。

    图看起来很简单,不就是一个label显示用户名称,一个imageView显示用户的icon嘛?或者有些人觉得一个button也可以搞定,但是我们先了解一下需求:

    1)需要考虑是否是大v,然后确定是否显示icon

    2)名字的文字内容多少是不固定的,有的长又的短,超过限制宽度需要使用...代替

    3)如果是大v,那么名字和icon整体的显示要居中显示,如果不是大v,那么名字单独居中,icon不显示

    分析之后,想想只使用一个button或者使用labelimageView可以实现,但是有点小麻烦,一起来看一下:

    实现效果

    20180705114934724.png

    一个tableView简单的展示相关的一些显示情况,具体看一下是如何布局的:

    1、添加子视图,包括两个部分,头像+容器视图

    func setupSubViews() {
        containView.addSubview(avatar)
        contentView.addSubview(containView)
     
        // 头像约束
        avatar.translatesAutoresizingMaskIntoConstraints = false
        avatar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        avatar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15).isActive = true
     
        // 容器视图添加约束
        containView.translatesAutoresizingMaskIntoConstraints = false
        containView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 20).isActive = true
        containView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: 20).isActive = true
        containView.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 10).isActive = true
        containView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
        containView.backgroundColor = UIColor.cyan
    }
    

    2、布局容器视图

    func setupContainView() {
         // 添加子视图
         containView.addSubview(nameLabel)
         containView.addSubview(img)
     
         // 给label添加约束
         nameLabel.translatesAutoresizingMaskIntoConstraints = false
         nameLabel.leadingAnchor.constraint(greaterThanOrEqualTo: containView.leadingAnchor).isActive = true
         nameLabel.topAnchor.constraint(equalTo: containView.topAnchor).isActive = true
         nameLabel.bottomAnchor.constraint(equalTo: containView.bottomAnchor).isActive = true
         nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor) // 引用约束
         nameTrailingConstraint.isActive = true
     
         // 给imageView添加约束
         img.translatesAutoresizingMaskIntoConstraints = false
         img.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor).isActive = true
         img.trailingAnchor.constraint(lessThanOrEqualTo: containView.trailingAnchor).isActive = true
     
         // 设置label的内容压缩较低,当内容过多时,压缩label的显示内容
         nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    }
    

    3、添加nameLabel的布局以实现居中显示

    func showVIPIcon(_ show: Bool) {
        // 失效原有约束
        NSLayoutConstraint.deactivate([nameTrailingConstraint])
        if show {
           nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor, constant: 0)
        } else {
           // 17是图片的大小,相当于盖住图片
           nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor, constant: 17)
        }
        // 生效新的约束
        NSLayoutConstraint.activate([nameTrailingConstraint])
        img.isHidden = !show
    }
    

    4、在代理方法中赋值显示内容

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
          let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
          cell.nameLabel.text = names[indexPath.row]
          cell.showVIPIcon(vips[indexPath.row])
          return cell
    }
    

    当然也可以不使用容器视图,单独对nameLabel进行布局调整,有兴趣的可以尝试。但是我们在看一下使用UIStackView

    1、将avatarstackView添加到contentView上,然后只需要对avatarstackView布局设置属性即可

    func setupStackView() {
            contentView.addSubview(avatar)
            contentView.addSubview(stackView)
     
            // 头像约束
            avatar.translatesAutoresizingMaskIntoConstraints = false
            avatar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
            avatar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15).isActive = true
     
            // 添加约束
            stackView.translatesAutoresizingMaskIntoConstraints = false
            stackView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 20).isActive = true
            stackView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: 20).isActive = true
            stackView.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 10).isActive = true
            stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
     
            stackView.axis = .horizontal // 水平布局
            stackView.alignment = .center
            stackView.spacing = 0 // 设置子控件之间的距离
            stackView.distribution = .equalCentering
     
            nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
            stackView.addArrangedSubview(nameLabel)
            stackView.addArrangedSubview(img)
    }
    

    2、是否显示大v,非常简单,控制图片的显示就好,因为stackView会自动根据是否显示图片来调整布局

    func showVIPIcon(_ show: Bool) {
          img.isHidden = !show
     }
    

    效果:

    20180705120605294.png

    UIStackView与枚举

    我们使用UIStackView的时候需要创建基本的UI控件,为了让创建UI控件更加简单,所以配合枚举进行使用,详情内容看这里,下面一起看一下代码实现

    1、定义枚举

    enum ContentElement {
        case label(String)
        case button(String, UIColor, () -> ())
        case image(UIImage)
    }
    

    2、扩展枚举,声明计算属性用于添加子视图

    // 定义枚举来创建UI
    extension ContentElement {
        var view: UIView {
            switch self {
            case .label(let text):
                let label = UILabel()
                label.numberOfLines = 0
                label.text = text
                return label
            case .button(let title, let titleColor, let callback):
                return CallbackButton(title: title, titleColor: titleColor, onTap: callback)
            case .image(let image):
                let imgView =  UIImageView(image: image)
                imgView.contentMode = .scaleAspectFit
                return imgView
            }
        }
    }
    

    3、处理button的响应事件

    final class CallbackButton: UIView {
        let onTap: () -> Void
        let button: UIButton
     
        init(title: String, titleColor: UIColor, onTap: @escaping () -> Void) {
            self.onTap = onTap
            self.button = UIButton(type: .custom)
            super.init(frame: .zero)
            addSubview(button)
            button.translatesAutoresizingMaskIntoConstraints = false
            button.setTitle(title, for: .normal)
            button.setTitleColor(titleColor, for: .normal)
            button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
     
            button.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
            button.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
            button.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
            button.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        }
     
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
     
        @objc func tapped(sender: AnyObject) {
            onTap()
        }
    }
    

    4、扩展UIStackView,提供遍历的便捷化方法

    extension UIStackView {
        convenience init(elements: [ContentElement]) {
            self.init()
            translatesAutoresizingMaskIntoConstraints = false
            for element in elements {
                addArrangedSubview(element.view)
            }
        }
    }
    

    5、简单使用一下

    class UIStackViewController: UIViewController {
     
        override func viewDidLoad() {
            super.viewDidLoad()
     
            let elements: [ContentElement] = [
                .image(UIImage(named: "avatar")!),
                .label("To use the Swift Talk app please login as a subscriber"),
                .button("Login with GitHub", UIColor.red,{
                    print("Button tapped")
                }),
                .label("If you're not registered yet, please visit http://objc.io for more information")
            ]
     
            let stack = UIStackView(elements: elements)
            stack.axis = .vertical
            stack.spacing = 10
            view.addSubview(stack)
     
            stack.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor).isActive = true
            stack.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor).isActive = true
            stack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
            stack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        }
    }
    

    效果如下


    20180706120516190.png

    参考

    UIStackView

    相关文章

      网友评论

        本文标题:iOS - UIStackView的使用

        本文链接:https://www.haomeiwen.com/subject/dcunqctx.html