VFL(Visual Format Language)允许你使用一种ASCII格式的字符串定义约束. 通过一行代码, 你可以在水平或者垂直方向上指定多个约束, 这跟一次只能创建一个约束相比会节省大量的代码量. 在本文中, 你将会和VFL亲密接触, 主要内容包括:
- 创建水平和垂直约束
- 在VFL中使用
views
- 在VFL中使用
metrics
- 使用layout options布局界面
- 使用layout guides
注意:
- Xcode 8.0 (8A218a), iOS 10.0, and Swift 2.3
- 阅读本文之前假设你已经熟知自动布局. 如果对此你真的还一无所知, 你需要先阅读Auto Layout Tutorial Part 1: Getting Started和Auto Layout Tutorial Part 2: Constraints
开始
首先下载本文所使用的工程文件, 下载完成后运行, 结果如下:
好吧! 看着真是一团糟. 为什么会是这样呢? 因为这里还没有设置任何约束在里面, 所以所有的组件都集中显示在视图的最上面和最左边, 那么本文将会一步步创建约束来让界面变得合理. 期待吧?
Visual Format String 语法
在开始处理布局和约束之前, 你需要先了解一些VFL的基本知识, 首先需要知道的就是VFL字符串可以拆解成如下结构:
下面我们一一解释其中的含义:
-
约束的方向, 非必选参数, 可取的值:
- H: 指定水平方向
- V: 指定垂直方向
- 不指定: 不指定方向时默认为水平方向
-
上边缘&前边缘与父视图的联系, 非必选参数:
- 当前view的上边缘和其父视图上边缘的间距(垂直)
- 当前view的前(左)边缘和其父视图前(左)边缘的间距(水平)
正在布局的view, 必选参数
与另一个view的联系, 非必选参数
-
下边缘&后边缘与父视图的联系, 非必选参数:
- 当前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
, appNameLabel
和skipButton
, 运行程序, 效果如下:
界面清爽不少吧! 将下面的代码继续添加到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)
下面让我来解释下上面这段代码的含义吧:
创建一个包含各个视图的字典, 以便在VFL字符串中可以指代特定的视图.
创建一个存放
NSLayoutConstraint
的可变数组, 之后会往里添加所创建的约束.给
iconImageView
创建垂直方向的约束, 高度30, 上边缘距离其父视图的上边缘距离为20.给
appNameLabel
创建垂直方向的约束, 上边缘距离其父视图的上边缘距离为23.给
skipButton
创建垂直方向的约束, 上边缘距离其父视图的上边缘距离为20.给
iconImageView
appNameLabel
和skipButton
同时设置水平方向的约束.iconImageView
的前边缘距离其父视图的前边缘距离为15, 宽度为30. 下面,iconImageView
和appNameLabel
的间距为标准间距(8). 下面,appNameLabel
和skipButton
的间距为标准间距(8). 最后,skipButton
的后边缘和其父视图的后边缘间距为15.使用
NSLayoutConstraint
提供的类方法activateConstraints(_:)
激活约束, 这里需要将所有的约束传递进去.
注意: views
中存放的键值对和VFL中使用的字符串必须一一对应, 否则系统不知道你指代的是哪个视图, 随之就是程序崩溃.
运行程序, 效果如下:
怎么样? 是不是好看一些了! 尝到甜头, 那我们继续!
下面我们开始布局之前被我们隐藏起来的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
summaryLabel
和 pageControl
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
下面让我来一步步解释下上面这段代码的含义吧:
给
summaryLabel
和welcomeLabel
设置水平约束, 让他们的前边缘和后边缘都距离其父视图的前后边缘15.设置
iconImageView
和appImageView
在垂直方向上间距10.设置
appImageView
和welcomeLabel
在垂直方向上间距10.设置
welcomeLabel
和summaryLabel
在垂直方向上间距4.设置
summaryLabel
和pageControl
在垂直方向上间距15, 并且设置pageControl
的宽度为9,pageControl
的后边缘距离其父视图的后边缘距离为15.
运行程序, 效果如下:
看着像模像样了吧? 但是为什么图片和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
上面移除的代码把appNameLabel
和skipButton
的垂直约束去掉了, 下面你会使用布局选项来设置它们在垂直方向上的位置.
找到创建了topRowHorizontalConstraints
的代码, 设置其options
的参数为[.AlignAllCenterY]
, 改完之后代码如下:
let topRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|",
options: [.AlignAllCenterY], metrics: nil, views: views)
当设置了.AlignAllCenterY
后, VFL字符串中提到的每一个视图都会在垂直方向上对齐. 这段代码之所有生效是因为iconImageView
在垂直方向上的约束已经定义好了. 所以NameLabel
和skipButton
就在垂直方向上和iconImageView
对齐.
如果现在运行程序, 那么效果和没改之前是一样的, 但是现在的代码更酷, 不是吗?
下面把创建了welcomeHorizontalConstraints
约束的代码删掉, 这样welcomeLabel
在水平方向的约束就没有了. 然后修改一下summaryLabelVerticalConstraints
的代码:
summaryLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:[welcomeLabel]-4-[summaryLabel]", options: [.AlignAllLeading, .AlignAllTrailing],
metrics: nil, views: views)
上面这段代码设置了options
的值为[.AlignAllLeading, .AlignAllTrailing]
. 运行程序, 效果就是welcomeLabel
和summaryLabel
的前边缘和后边缘都距离各自父视图的前后边缘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)
这句代码的含义你应该知道了吧? 运行一下, 看看效果!
怎么样? 居中了吧!
注意: 要使用布局选项的条件是至少有一个视图已经完全设置好了约束. 这样其他视图才能有参照物. 比如给你看个典型的反例:
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]
现在就可以在创建topRowHorizontalConstraint
和summaryHorizontalContraints
的代码中使用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的键值对取代了之前的硬编码, 是不是感觉很棒?
更详细的内容, 可以点击原文地址进一步了解.