Osheep

时光不回头,当下最重要。

Auto Layout Visual Format Language使用指引

VFL(Visual Format Language)允许你使用一种ASCII格式的字符串定义约束. 通过一行代码, 你可以在水平或者垂直方向上指定多个约束, 这跟一次只能创建一个约束相比会节省大量的代码量. 在本文中, 你将会和VFL亲密接触, 主要内容包括:

  • 创建水平和垂直约束
  • 在VFL中使用views
  • 在VFL中使用metrics
  • 使用layout options布局界面
  • 使用layout guides

注意:

  1. Xcode 8.0 (8A218a), iOS 10.0, and Swift 2.3
  2. 阅读本文之前假设你已经熟知自动布局. 如果对此你真的还一无所知, 你需要先阅读Auto Layout Tutorial Part 1: Getting StartedAuto Layout Tutorial Part 2: Constraints

开始

首先下载本文所使用的工程文件, 下载完成后运行, 结果如下:

《Auto Layout Visual Format Language使用指引》

-c

好吧! 看着真是一团糟. 为什么会是这样呢? 因为这里还没有设置任何约束在里面, 所以所有的组件都集中显示在视图的最上面和最左边, 那么本文将会一步步创建约束来让界面变得合理. 期待吧?

Visual Format String 语法

在开始处理布局和约束之前, 你需要先了解一些VFL的基本知识, 首先需要知道的就是VFL字符串可以拆解成如下结构:

《Auto Layout Visual Format Language使用指引》

-c

下面我们一一解释其中的含义:

  1. 约束的方向, 非必选参数, 可取的值:
    • H: 指定水平方向
    • V: 指定垂直方向
    • 不指定: 不指定方向时默认为水平方向
  2. 上边缘&前边缘与父视图的联系, 非必选参数:
    • 当前view的上边缘和其父视图上边缘的间距(垂直)
    • 当前view的前(左)边缘和其父视图前(左)边缘的间距(水平)
  3. 正在布局的view, 必选参数

  4. 与另一个view的联系, 非必选参数

  5. 下边缘&后边缘与父视图的联系, 非必选参数:

    • 当前view的下边缘和其父视图下边缘的间距(垂直)
      • 当前view的后(右)边缘和其父视图后(右)边缘的间距(水平)

上图中还包括两个橙色的字符, 其含义如下:

  • ?在VFL字符串中为非必选参数
  • *在VFL字符串中可能出现0次或者很多次

可用的字符

VFL中使用一些字符来描述布局:

  • | 父视图

  • - 标准间隔(通常为8像素)

  • == 宽度相等(可省略)

  • -20- 非标准间隔(20像素)

  • <= 小于或等于

  • >= 大于或等于

  • @250 约束的优先顺序, 取值范围为0-1000, 越大的值代表系统会优先满足该约束

    • 250 低优先顺序
    • 750 高优先顺序
    • 1000 必须满足的优先顺序

举例

H:|-[icon(==iconDate)]-20-[iconLabel(120@250)]-20@750-[iconDate(>=50)]-|

下面一步步来分析哦:

  • H 水平方向的约束

  • |-[icon icon的前边缘和它父视图的前边缘的间距为标准间距(8)

  • ==iconDate icon的宽等于iconDate的宽

  • ]-20-[iconLabel icon的后边缘距离iconLabel的前边缘为20

  • [iconLabel(120@250)] iconLabel的宽为120. 优先级为低, 如果自动布局有冲突时, 该条约束就有可能失效

  • -20@750- iconLable的后边缘到iconDate的前边缘距离为20. 优先级为高, 自动布局发生冲突时该条约束也不会失效

  • [iconDate(>=50)] iconDate的宽大于或等于50

  • -| iconDate的后边缘距离其父视图的距离为标准距离(8)

现在你已经对VFL有了基本的了解了吧? 下面是时候学以致用了.

创建约束

苹果的NSLayoutConstraint类提供了constraintsWithVisualFormat类方法用于创建约束, 你需要在工程文件中使用它来创建约束.
在Xcode中打开ViewController.swift, 将下面的代码添加到viewDidLoad()方法中:

appImageView.hidden = true
welcomeLabel.hidden = true
summaryLabel.hidden = true
pageControl.hidden = true

上面的代码将其余四个控件先隐藏, 只显示出iconImageView, appNameLabelskipButton, 运行程序, 效果如下:

《Auto Layout Visual Format Language使用指引》

-c

界面清爽不少吧! 将下面的代码继续添加到viewDidLoad()方法中:

// 1
let views = ["iconImageView": iconImageView,
  "appNameLabel": appNameLabel,
  "skipButton": skipButton]

// 2
var allConstraints = [NSLayoutConstraint]()

// 3
let iconVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-20-[iconImageView(30)]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += iconVerticalConstraints

// 4
let nameLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-23-[appNameLabel]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += nameLabelVerticalConstraints

// 5
let skipButtonVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-20-[skipButton]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += skipButtonVerticalConstraints

// 6
let topRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|",
  options: [],
  metrics: nil,
  views: views)
allConstraints += topRowHorizontalConstraints

// 7
NSLayoutConstraint.activateConstraints(allConstraints)

下面让我来解释下上面这段代码的含义吧:

  1. 创建一个包含各个视图的字典, 以便在VFL字符串中可以指代特定的视图.

  2. 创建一个存放NSLayoutConstraint的可变数组, 之后会往里添加所创建的约束.

  3. iconImageView创建垂直方向的约束, 高度30, 上边缘距离其父视图的上边缘距离为20.

  4. appNameLabel创建垂直方向的约束, 上边缘距离其父视图的上边缘距离为23.

  5. skipButton创建垂直方向的约束, 上边缘距离其父视图的上边缘距离为20.

  6. iconImageView appNameLabelskipButton同时设置水平方向的约束. iconImageView的前边缘距离其父视图的前边缘距离为15, 宽度为30. 下面, iconImageViewappNameLabel的间距为标准间距(8). 下面, appNameLabelskipButton的间距为标准间距(8). 最后, skipButton 的后边缘和其父视图的后边缘间距为15.

  7. 使用 NSLayoutConstraint 提供的类方法 activateConstraints(_:) 激活约束, 这里需要将所有的约束传递进去.

注意: views中存放的键值对和VFL中使用的字符串必须一一对应, 否则系统不知道你指代的是哪个视图, 随之就是程序崩溃.

运行程序, 效果如下:

《Auto Layout Visual Format Language使用指引》

-c

怎么样? 是不是好看一些了! 尝到甜头, 那我们继续!
下面我们开始布局之前被我们隐藏起来的4个视图, 首先选把之前添加上去的隐藏代码从viewDidLoad() 方法中删掉, 没错, 就是下面这四行:

appImageView.hidden = true
welcomeLabel.hidden = true
summaryLabel.hidden = true
pageControl.hidden = true

下面在views 字典中添加新的视图, 或者直接替换下面的代码:

let views = ["iconImageView": iconImageView,
                "appNameLabel": appNameLabel,
                "skipButton": skipButton,
               "appImageView": appImageView,
                  "welcomeLabel": welcomeLabel,
              "summaryLabel": summaryLabel,
              "pageControl": pageControl]

这里你在view字典中添加了appImageView welcomeLabel summaryLabelpageControl 4个视图, 那么现在你就可以在VFL字符串中使用调用这几个视图了.

将下面的代码添加到viewDidLoad方法中, 注意的是要添加到activateConstraints()之前:

// 1
let summaryHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-15-[summaryLabel]-15-|",
  options: [],
  metrics: nil,
  views: views)
allConstraints += summaryHorizontalConstraints

let welcomeHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-15-[welcomeLabel]-15-|",
  options: [],
  metrics: nil,
  views: views)
allConstraints += welcomeHorizontalConstraints

// 2
let iconToImageVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:[iconImageView]-10-[appImageView]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += iconToImageVerticalConstraints

// 3
let imageToWelcomeVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:[appImageView]-10-[welcomeLabel]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += imageToWelcomeVerticalConstraints

// 4
let summaryLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:[welcomeLabel]-4-[summaryLabel]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += summaryLabelVerticalConstraints

// 5
let summaryToPageVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:[summaryLabel]-15-[pageControl(9)]-15-|",
  options: [],
  metrics: nil,
  views: views)
allConstraints += summaryToPageVerticalConstraints

下面让我来一步步解释下上面这段代码的含义吧:

  1. summaryLabelwelcomeLabel 设置水平约束, 让他们的前边缘和后边缘都距离其父视图的前后边缘15.

  2. 设置iconImageViewappImageView在垂直方向上间距10.

  3. 设置appImageViewwelcomeLabel在垂直方向上间距10.

  4. 设置welcomeLabelsummaryLabel在垂直方向上间距4.

  5. 设置summaryLabelpageControl在垂直方向上间距15, 并且设置pageControl的宽度为9, pageControl的后边缘距离其父视图的后边缘距离为15.

运行程序, 效果如下:

《Auto Layout Visual Format Language使用指引》

-c

看着像模像样了吧? 但是为什么图片和page control没有居中显示呢? 别急, 下一个部分我们来细说这个问题!

布局选项

布局属性(Layout options)可以让我们在之前已经定义的垂直或者水平约束基础上再独立的设置约束.

下面就让你看看怎么使用这些布局属性吧, 首先将以下代码从viewDidLoad()方法里移除:

let nameLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-23-[appNameLabel]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += nameLabelVerticalConstraints

let skipButtonVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-20-[skipButton]",
  options: [],
  metrics: nil,
  views: views)
allConstraints += skipButtonVerticalConstraints

上面移除的代码把appNameLabelskipButton的垂直约束去掉了, 下面你会使用布局选项来设置它们在垂直方向上的位置.

找到创建了topRowHorizontalConstraints的代码, 设置其options的参数为[.AlignAllCenterY], 改完之后代码如下:

let topRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|",
  options: [.AlignAllCenterY], metrics: nil, views: views)

当设置了.AlignAllCenterY后, VFL字符串中提到的每一个视图都会在垂直方向上对齐. 这段代码之所有生效是因为iconImageView在垂直方向上的约束已经定义好了. 所以NameLabelskipButton就在垂直方向上和iconImageView对齐.

如果现在运行程序, 那么效果和没改之前是一样的, 但是现在的代码更酷, 不是吗?

下面把创建了welcomeHorizontalConstraints约束的代码删掉, 这样welcomeLabel在水平方向的约束就没有了. 然后修改一下summaryLabelVerticalConstraints的代码:

summaryLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:[welcomeLabel]-4-[summaryLabel]", options: [.AlignAllLeading, .AlignAllTrailing],
metrics: nil, views: views)

上面这段代码设置了options的值为[.AlignAllLeading, .AlignAllTrailing]. 运行程序, 效果就是welcomeLabelsummaryLabel的前边缘和后边缘都距离各自父视图的前后边缘15. 因为之前summaryLabel在水平方向的约束已经设好, 所以welcomeLabel在水平方向会和summaryLabel对齐.

同样这个效果和删除welcomeLabel水平约束前是一样的, 但是代码更简洁了.

下面再改一下summaryToPageVerticalConstraints的代码:

let pageControlVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:[summaryLabel]-15-[pageControl(9)]-15-|", options: [.AlignAllCenterX], metrics: nil,
views: views)

修改完的代码所产生的效果就是pageControl的中点在水平方向和summaryLabel的中点对齐, 代码之所以生效是因为summaryLabel的约束已经预先设定好了.

下面再改一下imageToWelcomeVerticalConstraints的代码:

let imageToWelcomeVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:[appImageView]-10-[welcomeLabel]", options: [.AlignAllCenterX], metrics: nil, 
views: views)

这句代码的含义你应该知道了吧? 运行一下, 看看效果!

《Auto Layout Visual Format Language使用指引》

-c

怎么样? 居中了吧!

注意: 要使用布局选项的条件是至少有一个视图已经完全设置好了约束. 这样其他视图才能有参照物. 比如给你看个典型的反例:

NSLayoutConstraints.constraintsWithVisualFormat("V:[topView]-[middleView]-[bottomView]",
options: [.AlignAllLeading], metrics: nil, 
views: ["topView": topView, "middleView": middleView, "bottomView": bottomView"])

以上VFL语句中没有一个视图是已经设置好约束的, 所以options: [.AlignAllLeading]是不会起作用的!!!

下面来看一个新的概念 –> Metrics

Metrics

Metrics是一个字典, 里面可以存储一些数值, 这样存储之后就可以在VFL字符串中调用了. Metrics最有用的地方就是当你想设置一些标准的间隔或者要计算一些间隔(字符串中不能计算)时, 可以使用它.

下面在ViewController.swift中定义一个表示间隔距离的常量

private let horizontalPadding: CGFloat = 15.0

然后创建我们的Metrics字典

let metrics = ["hp": horizontalPadding, "iconImageViewWidth": 30.0]

现在就可以在创建topRowHorizontalConstraintsummaryHorizontalContraints的代码中使用metrics了:

let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-hp-[iconImageView(iconImageViewWidth)]-[appNameLabel]-[skipButton]-hp-|",
  options: [.AlignAllCenterY],
  metrics: metrics,
  views: views)

let summaryHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "H:|-hp-[summaryLabel]-hp-|",
  options: [],
  metrics: metrics,
  views: views)

现在我们已经用metrics的键值对取代了之前的硬编码, 是不是感觉很棒?

更详细的内容, 可以点击原文地址进一步了解.

点赞