iOS 12 Auto Layout界面自动布局系列4-使用VFL添加布局约束

本系列第一篇文章介绍了自动布局的基本原理,第二篇通过一个简单的例子演示了如何使用IB以可视化方式创建自动布局约束,第三篇使用代码直接创建NSLayoutConstraint实例来定义自动布局约束。本篇文章在第三篇文章的基础上,使用Visual Format Language(暂且翻译为可视化格式语言,简称VFL)创建约束。
在第三篇文章中,我们仅仅创建了4个视图,就需要创建20多个NSLayoutConstraint实例,而且每次创建NSLayoutConstraint实例时都需要传入7个参数(firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant),非常繁琐且容易出错。在实际项目中,视图的层次会更复杂,约束的数量就会成倍增加,有没有办法既直观又简单地创建约束?那你不妨试试VFL,这也是这篇文章的主题。
除了简化创建约束之外,当调试布局约束冲突时,Xcode控制台会以VFL格式输出布局约束及错误信息,例如:

Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this:
(1) look at each constraint and try to figure out which you don't expect; 
(2) find the code that added the unwanted constraint or constraints and fix it.

( "", 
"", 
"",
"", 
"",
"", 
"", 
"", 
"", 
"", 
"", 
""`` )

Will attempt to recover by breaking constraint

所以,学习VFL还是很有必要的。不过既然是一门语言,必然就有其语法要求。。。
又要学语法。。。你是逗我吗?
好吧,我们还是以之前的?边做边讲吧。
纵屏

横屏

打开Xcode(版本10.2),新建Single View Application项目,项目命名为AutoLayoutByVFL,语言任意选择(本文使用Objective-C),设备选择Universal。下载苹果Logo图片apple.jpg,并将其拖入项目中。文件下载地址:
http://yunpan.cn/cfmJB82dfSwf6(提取码:4049)

界面上方用来显示苹果Logo图片的是一个UIImageView,其具有如下4个约束:

  • logoImageView左侧与父视图左侧对齐
  • logoImageView右侧与父视图右侧对齐
  • logoImageView顶部与父视图顶部对齐
  • logoImageView高度为父视图高度一半

将ViewController类的viewDidLoad方法修改如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];
    
    //水平方向上,logoImageView左侧与父视图左侧对齐,logoImageView右侧与父视图右侧对齐
    NSArray* hConstraintArray = [NSLayoutConstraint 
        constraintsWithVisualFormat:@"H:|-0-[logoImageView]-0-|" 
        options:0 
        metrics:nil 
        views:@{@"logoImageView": logoImageView}];
    [NSLayoutConstraint activateConstraints:hConstraintArray];
    
    //垂直方向上,logoImageView顶部与父视图顶部对齐
    NSArray* vConstraintArray = [NSLayoutConstraint 
        constraintsWithVisualFormat:@"V:|-0-[logoImageView]" 
        options:0 
        metrics:nil 
        views:@{@"logoImageView": logoImageView}];
    [NSLayoutConstraint activateConstraints:vConstraintArray];
    
    //logoImageView高度为父视图高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint 
        constraintWithItem:logoImageView 
        attribute:NSLayoutAttributeHeight 
        relatedBy:NSLayoutRelationEqual 
        toItem:self.view 
        attribute:NSLayoutAttributeHeight 
        multiplier:0.5f constant:0.0f];
    heightConstraint.active = YES;
}

第6行,设置translatesAutoresizingMaskIntoConstraints属性为NO,防止隐式自动添加NSAutoresizingMaskLayoutConstraint约束(可查看本系列第三篇文章)。
第11行调用了NSLayoutConstraint类的静态方法constraintsWithVisualFormat:…,根据传入的VFL字符串生成若干约束,并以数组形式返回,该方法定义如下:

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format 
    options:(NSLayoutFormatOptions)opts 
    metrics:(NSDictionary *)metrics 
    views:(NSDictionary *)views;

参数format是一个符合VFL语法的字符串。上述代码中的“H:”表示本VFL字符串描述的是水平方向的约束,与之相对的是“V:”表示垂直方向。如果VFL字符串没有指明“H:”还是“V:”,则默认为水平方向。“|”表示父视图。VFL要求所有视图的名字必须放在中括号之内,[logoImageView]指代的就是logoImageView。“-0-”表示的是间距值为0。
所以@"H:|-0-[logoImageView]-0-|"表示在水平方向上,logoImageView左侧与其父视图左侧的间距为0,logoImageView右侧与其父视图右侧的间距为0。同样的,@"V:|-0-[logoImageView]"表示在垂直方向上,logoImageView顶部与其父视图顶部的间距为0。
另外说一句,“-0-”可以不写,即如果间距为0则不用明确写出,所以@"H:|-0-[logoImageView]-0-|"可以精简为@“H:|[logoImageView]|”,@"V:|-0-[logoImageView]"可以精简为@“V:|[logoImageView]”,是不是很直观?

参数opts是一个位掩码变量(Bitmask),指定VFL中所有视图的对齐方式与方向:

typedef NS_OPTIONS(NSUInteger, NSLayoutFormatOptions)
{
    NSLayoutFormatAlignAllLeft = (1 << NSLayoutAttributeLeft),
    NSLayoutFormatAlignAllRight = (1 << NSLayoutAttributeRight),
    NSLayoutFormatAlignAllTop = (1 << NSLayoutAttributeTop),
    NSLayoutFormatAlignAllBottom = (1 << NSLayoutAttributeBottom),
    NSLayoutFormatAlignAllLeading = (1 << NSLayoutAttributeLeading),
    NSLayoutFormatAlignAllTrailing = (1 << NSLayoutAttributeTrailing),
    NSLayoutFormatAlignAllCenterX = (1 << NSLayoutAttributeCenterX),
    NSLayoutFormatAlignAllCenterY = (1 << NSLayoutAttributeCenterY),
    NSLayoutFormatAlignAllBaseline = (1 << NSLayoutAttributeBaseline),
    NSLayoutFormatAlignAllLastBaseline = NSLayoutFormatAlignAllBaseline,
    NSLayoutFormatAlignAllFirstBaseline NS_ENUM_AVAILABLE_IOS(8_0) = (1 << NSLayoutAttributeFirstBaseline),
    
    NSLayoutFormatAlignmentMask = 0xFFFF,
    
    /* choose only one of these three */
    NSLayoutFormatDirectionLeadingToTrailing = 0 << 16, // default
    NSLayoutFormatDirectionLeftToRight = 1 << 16,
    NSLayoutFormatDirectionRightToLeft = 2 << 16,  
    
    NSLayoutFormatDirectionMask = 0x3 << 16,  
};

例如,对于@"|[progressView]|",opts取值NSLayoutFormatAlignAllCenterX,则表示progressView与其父视图左右均对齐,且二者水平中心对齐。
参数metrics是一个字典,用于对VFL字符串中的键(Key)进行替换。其中的键(Key)是字符串,出现在VFL语句中;值(Value)是NSNumber对象。例如,某个约束值是一个运行时变量,则可以使用metrics进行变量替换。例如,对于@“H:|-btnMargin-[submitButton(>=btnMinWidth, <=btnMaxWidth)]”,表示submitButton与其父视图左侧间距为btnMargin,且最小宽度为btnMinWidth,最大宽度为btnMaxWidth。那btnMargin、btnMinWidth和btnMaxWidth的值就可以通过在运行时构造一个字典,例如@{@“btnMargin”: @(self.view.bounds.width * 0.1), @“btnMinWidth”: @(self.view.bounds.width * 0.6), @“btnMaxWidth”: @(self.view.bounds.width * 0.8)}来动态指定。
在编写VFL字符串时,尽量避免包含无意义的数值(幻数Magic Number),而是应该通过对数值进行命名来说明数值的确切含义。通过metrics字典能够让编译器自动把命名(键)替换为其对应的数值,目的是使VFL更容易明白和理解。
另外,在解析VFL时,UIKit需要知道VFL字符串中的视图名称究竟对应哪个真实的视图,视图映射字典参数views就用来提供这个信息。其中的键(Key)是字符串,出现在VFL语句中;值(Value)是UIView对象。上述代码中的@{@“logoImageView”: logoImageView}表示的就是字符串@"logoImageView"对应视图logoImageView。
不过有个很遗憾的事实要告诉你,VFL并不能表达所有的约束,例如“logoImageView高度为父视图高度一半”这样的具有比例关系的约束,以及和安全区域相关的约束,就无法使用VFL表达出来,所以这时我们只能直接创建NSLayoutConstraint实例了,就像上面的代码那样。
接着我们添加UIScrollView,在viewDidLoad方法中添加如下代码:

    UIScrollView* scrollView = [UIScrollView new];
    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    scrollView.backgroundColor = [UIColor blueColor]; //为了方便查看效果,暂时将scrollView背景色设置为蓝色
    [self.view addSubview:scrollView];
    
    //水平方向上,scrollView左侧与父视图左侧对齐,scrollView右侧与父视图右侧对齐
    NSArray* hScrollViewConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(scrollView)];
    [NSLayoutConstraint activateConstraints:hScrollViewConstraintArray];
    
    //垂直方向上,scrollView顶部与logoImageView底部对齐,scrollView底部与父视图底部对齐
    NSArray* vScrollViewConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[logoImageView][scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(logoImageView, scrollView)];
    [NSLayoutConstraint activateConstraints:vScrollViewConstraintArray];

需要额外说明的是,这里调用了一个NSDictionaryOfVariableBindings宏,它能够方便我们构建字典参数。简单说来,NSDictionaryOfVariableBindings(scrollView)就等于@{@“scrollView”: scrollView},NSDictionaryOfVariableBindings(logoImageView, scrollView)就等于@{@“logoImageView”: logoImageView, @“scrollView”: scrollView}。
另外,在垂直方向上,我们可以把之前的@"V:|[logoImageView]"与刚才的@"V:[logoImageView][scrollView]|"合并为一句@“V:|[logoImageView][scrollView]|”,就不需要分别创建heightConstraint与vScrollViewConstraintArray了,达到进一步精简的目的。

接着我们添加scrollView中的两个UILabel对象,在viewDidLoad方法中添加如下代码:

    UILabel* nameLabel = [UILabel new];
    nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
    nameLabel.text = @"苹果公司";
    nameLabel.backgroundColor = [UIColor greenColor];
    [scrollView addSubview:nameLabel];
    
    UILabel* descriptionLabel = [UILabel new];
    descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
    descriptionLabel.text = @"苹果公司(Apple Inc. )是美国的一家高科技公司。由史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗·韦恩(Ron Wayne)等三人于1976年4月1日创立,并命名为美国苹果电脑公司(Apple Computer Inc. ), 2007年1月9日更名为苹果公司,总部位于加利福尼亚州的库比蒂诺。\n苹果公司创立之初主要开发和销售的个人电脑,截至2014年致力于设计、开发和销售消费电子、计算机软件、在线服务和个人计算机。苹果的Apple II于1970年代助长了个人电脑革命,其后的Macintosh接力于1980年代持续发展。该公司硬件产品主要是Mac电脑系列、iPod媒体播放器、iPhone智能手机和iPad平板电脑;在线服务包括iCloud、iTunes Store和App Store;消费软件包括OS X和iOS操作系统、iTunes多媒体浏览器、Safari网络浏览器,还有iLife和iWork创意和生产力套件。苹果公司在高科技企业中以创新而闻名世界。\n苹果公司1980年12月12日公开招股上市,2012年创下6235亿美元的市值记录,截至2014年6月,苹果公司已经连续三年成为全球市值最大公司。苹果公司在2014年世界500强排行榜中排名第15名。2013年9月30日,在宏盟集团的“全球最佳品牌”报告中,苹果公司超过可口可乐成为世界最有价值品牌。2014年,苹果品牌超越谷歌(Google),成为世界最具价值品牌 。";
    descriptionLabel.numberOfLines = 0;
    descriptionLabel.backgroundColor = [UIColor yellowColor];
    [scrollView addSubview:descriptionLabel];
    
    
    //水平方向上,nameLabel左侧与父视图左侧对齐,nameLabel右侧与父视图右侧对齐,nameLabel宽度与logoImageView宽度相同
    NSArray* hNameLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[nameLabel(==logoImageView)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(nameLabel, logoImageView)];
    [NSLayoutConstraint activateConstraints:hNameLabelConstraintArray];

    //水平方向上,descriptionLabel左侧与父视图左侧对齐,descriptionLabel右侧与父视图右侧对齐,descriptionLabel宽度与logoImageView宽度相同
    NSArray* hDescriptionLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[descriptionLabel(==logoImageView)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(descriptionLabel, logoImageView)];
    [NSLayoutConstraint activateConstraints:hDescriptionLabelConstraintArray];

    //垂直方向上,nameLabel顶部与父视图顶部对齐,nameLabel高度为20,nameLabel底部与descriptionLabel顶部对齐,descriptionLabel底部与父视图底部对齐
    NSArray* vLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[nameLabel(20)][descriptionLabel]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(nameLabel, descriptionLabel)];
    [NSLayoutConstraint activateConstraints:vLabelConstraintArray];

其中@"H:|[nameLabel(==logoImageView)]|"表示nameLabel宽度与logoImageView宽度相等,@"V:|[nameLabel(20)][descriptionLabel]|"表示nameLabel的宽度为20。到此我们就完成了这个例子,在此附上全部代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];
    
    UIScrollView* scrollView = [UIScrollView new];
    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:scrollView];
    
    //水平方向上,logoImageView左侧与父视图左侧对齐,logoImageView右侧与父视图右侧对齐
    NSArray* hConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[logoImageView]|" options:0 metrics:nil views:@{@"logoImageView": logoImageView}];
    [NSLayoutConstraint activateConstraints:hConstraintArray];
    
    //垂直方向上,logoImageView顶部与父视图顶部对齐,logoImageView底部与scrollView顶部对齐,scrollView底部与父视图底部对齐
    NSArray* vConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[logoImageView][scrollView]|" options:0 metrics:nil views:@{@"logoImageView": logoImageView, @"scrollView": scrollView}];
    [NSLayoutConstraint activateConstraints:vConstraintArray];
    
    //logoImageView高度为父视图高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];
    heightConstraint.active = YES;
    
    //水平方向上,scrollView左侧与父视图左侧对齐,scrollView右侧与父视图右侧对齐
    NSArray* hScrollViewConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(scrollView)];
    [NSLayoutConstraint activateConstraints:hScrollViewConstraintArray];
    
    UILabel* nameLabel = [UILabel new];
    nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
    nameLabel.text = @"苹果公司";
    nameLabel.backgroundColor = [UIColor greenColor];
    [scrollView addSubview:nameLabel];
    
    UILabel* descriptionLabel = [UILabel new];
    descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
    descriptionLabel.text = @"苹果公司(Apple Inc. )是美国的一家高科技公司。由史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗·韦恩(Ron Wayne)等三人于1976年4月1日创立,并命名为美国苹果电脑公司(Apple Computer Inc. ), 2007年1月9日更名为苹果公司,总部位于加利福尼亚州的库比蒂诺。\n苹果公司创立之初主要开发和销售的个人电脑,截至2014年致力于设计、开发和销售消费电子、计算机软件、在线服务和个人计算机。苹果的Apple II于1970年代助长了个人电脑革命,其后的Macintosh接力于1980年代持续发展。该公司硬件产品主要是Mac电脑系列、iPod媒体播放器、iPhone智能手机和iPad平板电脑;在线服务包括iCloud、iTunes Store和App Store;消费软件包括OS X和iOS操作系统、iTunes多媒体浏览器、Safari网络浏览器,还有iLife和iWork创意和生产力套件。苹果公司在高科技企业中以创新而闻名世界。\n苹果公司1980年12月12日公开招股上市,2012年创下6235亿美元的市值记录,截至2014年6月,苹果公司已经连续三年成为全球市值最大公司。苹果公司在2014年世界500强排行榜中排名第15名。2013年9月30日,在宏盟集团的“全球最佳品牌”报告中,苹果公司超过可口可乐成为世界最有价值品牌。2014年,苹果品牌超越谷歌(Google),成为世界最具价值品牌 。";
    descriptionLabel.numberOfLines = 0;
    descriptionLabel.backgroundColor = [UIColor yellowColor];
    [scrollView addSubview:descriptionLabel];
    
    //水平方向上,nameLabel左侧与父视图左侧对齐,nameLabel右侧与父视图右侧对齐,nameLabel宽度与logoImageView宽度相同
    NSArray* hNameLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[nameLabel(==logoImageView)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(nameLabel, logoImageView)];
    [NSLayoutConstraint activateConstraints:hNameLabelConstraintArray];

    //水平方向上,descriptionLabel左侧与父视图左侧对齐,descriptionLabel右侧与父视图右侧对齐,descriptionLabel宽度与logoImageView宽度相同
    NSArray* hDescriptionLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[descriptionLabel(==logoImageView)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(descriptionLabel, logoImageView)];
    [NSLayoutConstraint activateConstraints:hDescriptionLabelConstraintArray];

    //垂直方向上,nameLabel顶部与父视图顶部对齐,nameLabel高度为20,nameLabel底部与descriptionLabel顶部对齐,descriptionLabel底部与父视图底部对齐
    NSArray* vLabelConstraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[nameLabel(20)][descriptionLabel]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(nameLabel, descriptionLabel)];
    [NSLayoutConstraint activateConstraints:vLabelConstraintArray];
}

与第三篇文章中逐个去创建NSLayoutConstraint对象相比,是不是简单直观了不少?程序最终项目文件链接:
http://yunpan.cn/cVE8i8WmnpJwv(提取码:d348)
本篇文章我们初步了解了VFL的基本使用方式,关于VFL的具体语法格式请参看苹果的《Auto Layout Guide》,链接:
https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html#//apple_ref/doc/uid/TP40010853-CH16-SW1

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html#//apple_ref/doc/uid/TP40010853-CH27-SW1

在本系列后续文章中,我将继续介绍自动布局中的动画实现、事件处理、调试,以及Size Class的使用,敬请期待吧。

你可能感兴趣的:(iOS开发,Objective-C)