iOS开发自动布局(Autolayout)之VFL可视化格式语言(Visual Format Language)
名字起的这么霸气,是因为本文也许会是一篇最全面,最透彻,最有深度的VFL教程
Visual Format Language
Visual Format Language,简称VFL 即“可视化格式语言”。
VFL是一种声明性语言,VFL允许您通过一个格式化后的代码字符串迅速定义视图的自动布局约束。
VFL 原理
当通过constraintsWithVisualFormat方法制定VFL代码之后,SDK会使用特定的VFL语法分析器,格式字符串解析器(Parser)进行VFL代码语法分析(Syntactic analysis),也叫Parsing,VFL语法分析器,格式字符串解析器根据输入字符流从左向右解析(其实实现起来就是各种遍历,各种判断,各种正则)出一个个的操作动作和操作相关联的对象,并将其当做以下方法的参数
1 |
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c; |
或者更底层的API进行创建约束,从而得到与手动创建Auto Layout代码一样的效果
格式字符串解析基本思路
1.解析约束方向 V/H
2.解析[ ] 与 -
3.解析对象与第二个对象的关系 >=< 及优先级 及 边界 |
你完全可以根据这个思路自己实现一个类似constraintsWithVisualFormat方法
VFL API
声明:
1 |
+ (NSArray<__kindof NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary<NSString *,id> *)metrics views:(NSDictionary<NSString *, id> *)views; |
描述:
使用ASCII艺术花那样,可视化格式的字符串创建一个数组的约束
参数:
format 格式规范的约束。
opts 描述VFL中的所有对象的属性和布局的方向 。
metrics 一个字典,字典的key必须是在VFL字符串中被使用的字符串,值必须是NSNumber对象
views 出现在在VFL字符串中的视图组成的字典, 字典的key必须是在VFL字符串中被使用的字符串。并且其值必须是一个View对象
返回值
一组约束,组合,表达提供的视图与父视图之间VFL字符串所描述的约束。约束数组的顺序与在VFL中指定的顺序相同。
选项 NSLayoutFormatOptions
下表列出了可以用于可视化格式方法的选项。这些选项包括了对其掩码和格式方向掩码。可以提供一个格式方向掩码。在iOS7以前,苹果也要求同时只能选择一种对其掩码。现在不做该要求了。可以使用按位OR来连接这些选项。
对于大多数布局工作,只需要使用左列的值。这些选择设置了对齐方式,可以增强指定的可视化格式。在极少情况下,需要翻转格式方向,此时可以使用表右侧列中的选项。从前缘到后缘的方向是默认值。无需显示的设置该值。
对齐掩码 Alignment Masks | 格式方向掩码 Format Direction Masks |
NSLayoutFormatAlignAllLeft | NSLayoutFormatDirectionLeadingToTrailing |
NSLayoutFormatAlignAllRight | NSLayoutFormatDirectionLeftToRight |
NSLayoutFormatAlignAllTop | NSLayoutFormatDirectionRightToLeft |
NSLayoutFormatAlignAllBottom | |
NSLayoutFormatAlignAllLeading | |
NSLayoutFormatAlignAllTrailing | |
NSLayoutFormatAlignAllCenterX | |
NSLayoutFormatAlignAllCenterY | |
NSLayoutFormatAlignAllBaseline |
对齐
应当总是在格式中正交地应用对齐掩码。当创建一个水平行时,也就确定了一个垂直对其(反过来也一样)。例如设想将一行对象从左向右放置,例如 H:[view1]-[view2]-[view3]-[view4]。你只需要将它们向上或者向下调整一点,便可以对其它们的顶部,中间或者底部。
然而,无法对齐它们的左边或者右边,因为如果那么做的话,会强迫它们脱离指定的布局顺序,所有的视图会划像左边或者右边,其根本上是何可视化格式矛盾的。
违反这个规则会产生一个异常,我视图应用一个水平对齐(前缘对齐)到一个水平约束上 (H:[view1]-8-[view2]):
1 2 3 |
2013-01-22 12:35:23.885 HelloWorld[25429:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: Options mask required views to be aligned on a horizontal edge, which is not allowed for layout that is also horizontal. |
省略选项
通过给选项参数赋0值来忽略选项。NSLayoutConstraint根据提供的可视化格式来建立约束,但是没有添加任何约束:
1 2 3 4 |
[self.view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view1]-8-[view2]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view1, view2)]]; |
变量绑定 NSDictionaryOfVariableBindings
当使用VFL时,布局系统将名称字符串(例如“view1”“view2”)同他们表示的对象关联起来。变量绑定用于创建这些关联。可以通过调用一个定义在NSLayoutConstraint.h头文件中的宏NSDictionaryOfVariableBindings()来创建它们,向该宏传入任意个本地的师徒变量,如示例:
NSDictionaryOfVariableBindings(view1,view2,view3,view4)
正如所见,这个宏并不要求传入一个nill信号来结束列表。它根据传入的变量来建立一个字典,使用变量的名称作为key,使变量指向的对象作为值。例如这个调用:
NSDictionaryOfVariableBindings(leftLabel,rightLabel)
等同创建下列这个字典
@{@"leftLabel":leftLabel,@"rightLabel":rightLabel}
如果不想使用这个变量绑定宏,那么很容易自己创建一个字典,然后传给方法中。可视化约束对视图数组不太友好,这也是创建一个自定义保定字典的原因。
间接的问题
考虑如下代码,它试图围绕一个视图数组创建一个可视化格式。
1 2 3 4 |
NSArray *views = @[view1,view2]; NSDictionary *bindings = NSDictionaryOfVariableBindings(views[0],views[1]); NSArray *constraints=[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[view[0]]-[view[1]]" options:0 metrics:nil views:bindings]; [self.view addConstraints:constraints]; |
下面是绑定字典:
字典的key,分别是字符串"views[0]" ,"views[1]"
1 2 3 4 5 |
Printing description of bindings: { "views[0]" = "<UIView: 0x7ff682f19f20; frame = (0 0; 0 0); layer = <CALayer: 0x7ff682f163a0>>"; "views[1]" = "<UIView: 0x7ff682f1a090; frame = (0 0; 0 0); layer = <CALayer: 0x7ff682f0a7e0>>"; } |
他在运行时产生了一个异常。虽然key是字符串,value是字符串指向的对象,看起来没有问题,但是格式字符串解析器无法处理编入索引的视图引用。事实上,解析器认为你将Objective-C调用嵌入了该字符串,不过这一步对于NSLayoutConstraint类来说夸的太远了。无法正确解析
1 2 3 |
2016-06-24 08:56:31.708 Autolayout-Demo[701:7852] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: view is not a key in the views dictionary. H:|-[view[0]]-[view[1]] |
间接的替代方案
正如刚才看到的示例表明,可视化格式对于处理任何未在本地上下午显示声明的项有些力不从心。可视化格式依赖于简单的名称到实例的转换。使用代码指向非本地实例(例如:[self.view viewWithTag:99],或views[0],或者self.label2)的方式无法在格式字符串中正确解析。
为解决该问题,你可以使用代码手动创建绑定字典与格式化字符串:
1 2 3 |
NSDictionary *bindings = @{@"view1":views[0],@"view2":views[1]}; NSDictionary *bindings2 = @{@"label2":self.label2}; NSDictionary *bindings3 = @{@"view99":[self.view viewWithTag:99]}; |
如果视图数组中有很多视图,那直接遍历后动态拼接,组成绑定字典了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Initialize the format string and bindings dictionary NSMutableString *formatString = [NSMutableString string]; NSMutableDictionary *bindings = [NSMutableDictionary dictionary]; // View counter int i = 1; // Build a format string that lays out the views in a row // e.g. @"H:|-[view1]-[view2]-[view3]..." [formatString appendString:@"H:|-"]; for (UIView *view in views) { // Create a view name NSString *viewName = [NSString stringWithFormat:@"view%0d", i++]; // Add the new view to the layout string [formatString appendFormat:@"[%@]%@",viewName, (i <= views.count) ? @"-" : @""]; // Store the view and its name in the bindings dictionary bindings[viewName] = view; } // Build constraints with centerY alignment NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatAlignAllCenterY metrics:nil views:bindings]; // Install the constraints [self.view addConstraints:constraints]; |
为绑定字典中的每一个条目赋一个名称和条目,能够让你解决无法在格式字符串内索引项的问题。
度量 Metrics
当事先不知道一个常量的值时,度量字典可以为可视化字符串提供该值。例如:
@"V:[view1]-spacing-[view2]"
在此,单词spacing代表某个目前还未确定的间隔值。通过创建一个将字符串和它的值等同起来的字典传递该值。例如:该字典将spacing和数值10关联起来:
NSDictionary *metrics = @{@"spacing":@10};
将这个字典提供给可视化格式创建方法的metrice参数:
1 |
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view1]-spacing-[view2]" options:NSLayoutFormatAlignAllCenterX metrics:metrics views:bindings]; |
现实工作中的度量
在使用编程的方式创建约束时,度量非常有用。例如:考虑以下方法。它根据你提供的尺寸大小来约束一个视图的宽度。当编写这个方法时,你不知道会被提供什么视图,也不知道应该将它的宽度约束为多少:
1 2 3 4 5 6 7 |
- (void)constrainView:(UIView *)view toWidth:(CGFloat)width { NSString *formatString = @"H:[view(==width)]"; NSDictionary *bindings = NSDictionaryOfVariableBindings(view); NSDictionary *metrics = @{@"width":@(width)}; NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:formatString options:0 metrics:metrics views:bindings]; [view addConstraints:constraints]; } |
VFL 语法
(<方向>:)? (<父视图前><连接>)? [<被布局视图>]! (<连接><其他视图>)* (<连接><父视图后>)?
1 2 3 4 5
? 问号指的是一个可选项,不是必需的。* 指的是该项会出现0次或者多次。! 代表必需指定
虽然该定义看上去令人生畏,但是事实上这些字符串相当容易构建。
例如:
“H:|-[view1]-[view2]-(>=8)-[view3]-|”
VFL格式字符串:
1.方向:约束在坐标轴上的方向。H代表水平方向排列,V代表垂直方向排列,不指定代表默认值水平方向
2.父视图前:关联到父视图前边缘。 垂直方向上的被布局视图顶部边缘与父视图顶部边缘的距离,水平方向上的被布局视图前边缘与父视图前边缘的距离
3.被布局视图:你添加layout的视图。
4.其他视图:被布局视图关联的另外一个视图
5.父视图后:关联到父视图后边缘。垂直方向上的被布局视图底部边缘与父视图底部边缘的距离,水平方向上的被布局视图后边缘与父视图后边缘的距离
视图名称
视图的名称被一对方括号包围,例如你可以使用[thisView],[thatView]。当使用变量绑定字典时,视图的名称指的是本地变量名。
父视图
总是用一个特殊的字符 竖线“|”,来表示父视图,仅会在VFL的开头或者结尾出现。在开头时候,它恰好出现在水平或者垂直方向指定符之后(“V:|”或者“H:|”)。在结尾时,它恰好出现在引号之前(“.....|”)。
无需在绑定字典中命名父视图。AutoLayout知道“|”代表父视图。
使用父视图的典型包括:
- 延伸一个视图来适应它的父视图,例如“H:|[view]|”
- 将一个视图偏离它的父视图的某条变,例如“V:[view]-8-”
- 创建一列或一行与父视图对齐的视图,例如“V:[view1]-[view2]-[view3]-[view4]”
连接
连接指定视图直接的间隔。每个视图(包括父视图的引用)间的连接标明了要添加的间距。
空连接
空连接看起来像“ H:[view1][view2]”,在每个视图的方括号直接没有任何符号,没有指定任何东西,显示效果将是两个视图仅仅的贴在了一起
标准间隔
连接符 - 代表一个标准的固定间隔,比如“ H:[view1]-[view2]”,每个视图直接增加了一个小的间隙,尽管官方并没有文档化,但标准间隔一般对于视图到视图布局为8点,对于视图到父视图为20点。标准间隔确保相关联但是又完全分离的视图以足够的视觉间隔显示。
数字间隔
可以在两个方括号直接使用一对连字符中间夹着数字来设置准确的间隔大小,约束“ H:[view1]-30-[view2]”在两个视图之间增加了一个30点的间隔。
引用父视图
格式“ H:|[view1]-[view2]|”确定了以父视图开头的水平布局。父视图后紧跟的是第一个视图,然后是一个间隔和第二个视图,之后是一个竖线符也就是是父视图。
与父视图的间隔
格式“ H:|-[view1]-[view2]-|”为前后边缘到父视图,每个增加了一个标准间隔 20点
格式“ H:|[view1]-[view2]-50-|”为后边缘到父视图,增加了一个自定义间隔 50点
灵活间隔
如果是目标是在视图间添加一个灵活间隔,同也有办法。为两个视图添加一个关系规则(例如“H:|-[view1]-(>=0)-[view2]-|”),允许这两个视图保持他们的尺寸,在维护他们的边缘同父视图的间隔时将他们分离出来。
这个规则可以读作“至少0点间距”,提供了使视图分开的更灵活的方式。
当说“至少50点”(>50 )或者“不超过30带你”(<=30)时,不能将间距和标准间隔结合起来是有。例如>=-,<=1是非法的。你只能使用等值数值。记住,标准的视图到时间间隔是8点,而视图到父视图的间隔是20带你。
圆括号
为了清晰可见,关系 (>=0) 被放在一堆括号中,括号将非常简单的正数或者负数或者度量名间隔区分开来
在如何表达规则时,你可以对可视化约束有很大的灵活性。如下规则都实现两个相互眦邻的视图:
- [view1][view2]
- [view1]-0-[view2]
- [view1]-(0)-[view2]
- [view1]-(==0)-[view2]
- [view1]-(>=0,<=0)-[view2]
- [view1]-(==0@1000)-[view2]
- [view1]-(>=0,<=0,<=30)-[view2]
当在括号中添加多个关系时,使用逗号分隔各项。符号@表示一个优先级
负数
比如为任何使用负数值的间隔加上括号。如下约束是非法的:
- V:[view1]--5-[view2]
由这些约束产生的错误如下、第一项抱怨符号“-”重复出现
1 2 |
2013-01-31 18:52:27.735 HelloWorld[88684:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: Cannot tell if this - is a minus sign or an accidental extra bar in the connection. Use parentheses around negative numbers. V:[v1]--5-[v2] |
第二项补获了负号前的间隔:
1 |
2013-01-31 18:53:18.756 HelloWorld[88713:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: Expected a number or key from the metrics dictionary, but encountered something else V:[v1]- -5-[v2] |
负数在可视化约束中是允许的,正确写法
- V:[view1]-(-5)-[view2]
优先级
在任何连接或者尺寸规则后拼接一个@符号,@符号后是一个想要设置的优先级。为了清晰可见,最好将其使用圆括号括起来
例如:
“H:|-(5@20)[view1]-[view2]-|”
间隔规则优先级默认是必须的(值为1000),因为将优先级降到了20,所以两个视图改变尺寸的方式可以能会影响布局。
多视图
格式视图并不局限于一个或者两个视图,我可以很容易的插入第三个,第四个或者更多。
“H:|-[view1]-[view2]-(>=8)-[view3]-|”
视图尺寸
VFL除了可以用方括号分隔视图名称外,还可以在方括号中指定视图的尺寸。可以在名称之后的圆括号里指定值,例如:
- 可以指定一个视图的宽度为120点固定值:“H:[view1(120)]”。如果你喜欢显示的说明关系,也可以添加这样的格式“H:[view1(==120)]”
- 可以指定一个视图的宽度至少50点,使用如下约束:“H:[view1(>=50)]”,50-70点之间“H:[view1(>=50,<=70)]”
- 可以引用其他视图指定尺寸:”H:|[view1(view2)-[view2]-|]“,因为约束是无方向的,所以可以自我引用,可以使用如下方式创建一个相等布局:”H:|[view1(view2)-[view2(view2)]-|]“,循环定义不会产生性能损失。
- 不是所有视图都需要参与尺寸匹配。一个请求可以围绕一个基本视图创建侧翼视图
- 视图尺寸也可以表示优先级。在格式化字符串“H:|[view1(==250@700)]-[view2(==250@701)]-|”中,view1和view2都请求宽度为250点,view2获胜,因为他的请求有更大的优先级,所以首先拉伸view2
- 尽管可以很容易地在代码中使用乘数来创建表达相对尺寸的约束,但是无法在可视化约束中那样做。这是一个非法约束::”H:|[view1==2*view2]-[view2]-|“。如果想说“view1宽度是view2的两倍”,就需要在代码中实现
出错
Xcode没有提供确保格式字符串有效性的编译器检查。一个糟糕的格式字符串会在运行时抛出异常。例如你可能忽略了一个冒号,如下:
1 2 |
2013-01-23 11:40:17.169 HelloWorld[35717:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: Expected ':' after 'H' to specify horizontal arrangement H|[view1]-[view2] |
或者在视图声明后忘记闭合方括号了。在这种情况下,会出现一个较长的日志,试图尽可能的指导你如何修复错误,如下所示:
1 2 |
2013-01-23 11:41:35.897 HelloWorld[35750:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format: Expected a ']' here. That is how you give the end of a view. H:|[view1-[view2] |
为何不能分布视图
在当前约束系统下,无法沿着某个坐标轴等间隔地分布视图。添加一个“[A]-(>=0)-[B]-(>=0)-[C]”约束并不难像你所期望的那样,沿着某个坐标轴等间隔的分布视图。基本的原因就在于每个约束同时只能引用两个视图。
等间隔或者分布规则必须至少引用3个对象。你不能说“view1和view2之间的间隔等于view2和 view3之间的间隔“,你也不能说他们的前缘,顶端,中心或者其他几何属性的距离。在任何NSLayoutConstraint实例中不存在足够的引用来讲规则包装成如下:
某种距离,类似A.center +distance = B.center和B.center+distance = C.center
同样也不能声明使两个约束的constant属性相等的联立方程,例如:
A.center == B.center+spacer1;
B.center = C.center+spacer2;
space1 = spacer2;
在iOS约束系统采用的yRmx +b 格式中(b偏移值必须已知,关系只存在于视图的属性之间)无法表示这些关系。你能做的最好办法是计算你想让这些项目相距多远,然后使用一个乘数和偏移值来手动地修正每个视图的位置。
VFL 实例
。。。。
本文部分摘自 Auto Layout 开发秘籍
写作过程中发现了这篇文章,值得阅读 Auto Layout Visual Format Language Tutorial
转载请注明:天狐博客 » VFL(Visual Format Language)