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

开始

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

-c

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

Visual Format String 语法

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

-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, 运行程序, 效果如下:

-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中使用的字符串必须一一对应, 否则系统不知道你指代的是哪个视图, 随之就是程序崩溃.

运行程序, 效果如下:

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

运行程序, 效果如下:

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

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

-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的键值对取代了之前的硬编码, 是不是感觉很棒?

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

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

推荐阅读更多精彩内容