iOS9 - UIStackView

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的相关属性




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




Alignment: Alignment控制subView对齐方式


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




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




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


Fill:子视图填充StackView

Leading:靠左对齐。

Trailing:靠右对齐。

Center:子视图以中线为基准对齐。

Top:靠顶部对齐。

Bottom:靠底部对齐。

First Baseline:按照第一个子视图中文字的第一行对齐。

Last Baseline:按照最后一个子视图中文字的最后一行对齐。


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




Fill: 沿Axis方向填充所有区域,为了实现这个功能,UIStackView会扩大其中的一个子视图填充额外的区域,该view有着最低的水平内容紧靠优先级(lowest horizontal content hugging priority),如果所有的view的优先级相等,那么会扩大第一个view。如果设置了spacing,那么这些 arrangedSubviews 之间的间距就是spacing。如果减去所有的spacing,所有的 arrangedSubview的固有尺寸( intrinsicContentSize)不能填满或者超出stackView的尺寸,那还是会按照 Hugging或者 CompressionResistance的优先级来拉伸或压缩一些 arrangedSubview。如果出现优先级相同的情况,就按排列顺序来拉伸或压缩。


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


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


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


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


Spacing 间距


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


subViewarrangedSubView


开始使用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的各种布局方式详情,看这里


动态的改变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添加约束即可。


实际应用场景

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


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

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

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

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

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

分析之后,想想只使用一个button有些要求是满足不了的,使用label和imageView可以实现,但是有点小麻烦,一起来看一下:

实现效果


一个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、将avatar和stackView添加到contentView上,然后只需要对avatar和stackView布局设置属性即可

   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
 }

效果:



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
    }
}

效果如下



兼容性


官方的 UIStackView只在 iOS9以上系统可以使用,但是其他的开源组织在低版本的系统上也实现了相应的功能:


TZStackView

OAStackView


参考


Auto Layout Without Constraints

iOS9 UIStackView 简介

UIStackView

Let's Build UIStackView




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值