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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343