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.

( "<NSLayoutConstraint:0x7fbb85999770 V:[UIView:0x7fbb85e8f850(1)]>", 
"<NSLayoutConstraint:0x7fbb85e24e80 V:[UIView:0x7fbb85e8f570(1)]>", 
"<NSLayoutConstraint:0x7fbb85e6f500 V:[UIImageView:0x7fbb85e79b80(200)]>",
"<NSLayoutConstraint:0x7fbb83758b80 V:[UIView:0x7fbb85da7610(44)]>", 
"<NSLayoutConstraint:0x7fbb8375a240 V:|-(0)-[UIView:0x7fbb85e8f850] (Names: '|':UIView:0x7fbb85e78d40 )>",
"<NSLayoutConstraint:0x7fbb8375a330 V:[UIView:0x7fbb85e8f850]-(20)-[UILabel:0x7fbb85e7e8a0'\U5c0f\U5446\U74dc\U6eda\U5440\U6eda']>", 
"<NSLayoutConstraint:0x7fbb8375a880 V:[UIImageView:0x7fbb85e79b80]-(3)-[UILabel:0x7fbb85e90010]>", 
"<NSLayoutConstraint:0x7fbb8375a8d0 V:[UILabel:0x7fbb85e90010]-(0)-[UIView:0x7fbb85da7610]>", 
"<NSLayoutConstraint:0x7fbb8375a920 V:[UIView:0x7fbb85da7610]-(0)-| (Names: '|':UIView:0x7fbb85e78d40 )>", 
"<NSLayoutConstraint:0x7fbb8375b0d0 V:|-(0)-[UIView:0x7fbb85e78d40] (Names: '|':UITableViewCellContentView:0x7fbb859a4e90 )>", 
"<NSLayoutConstraint:0x7fbb8375b170 UITableViewCellContentView:0x7fbb859a4e90.bottomMargin == UIView:0x7fbb85e78d40.bottom - 8>", 
"<NSLayoutConstraint:0x7fbb85a75650 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7fbb859a4e90(297.5)]>"`` )

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fbb85e6f500 V:[UIImageView:0x7fbb85e79b80(200)]>

所以,學習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的使用,敬請期待吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章