1. 程式人生 > >iOS 10 Auto Layout介面自動佈局系列3-使用原生NSLayoutConstraint添加布局約束

iOS 10 Auto Layout介面自動佈局系列3-使用原生NSLayoutConstraint添加布局約束

本系列的第一篇文章介紹了自動佈局的基本原理,第二篇文章通過一個簡單的例子演示瞭如何使用Xcode的Interface Builder(簡稱IB)以視覺化方式新增約束。本篇為該系列的第三篇文章,主要介紹如何通過寫程式碼來添加布局約束。
說句題外話,通過IB視覺化加約束,與寫程式碼加約束,這兩種方式各有優缺點。通過程式碼構建自動佈局約束是最基礎,也是最靈活的方式,但缺點是對於複雜介面相對繁冗而又容易出錯。而IB通過視覺化方式,把約束以直觀簡單的方式呈現出來,並且能夠在設計器中實時預覽佈局效果,但是缺點是並非所有的約束都能用IB來新增,而且不容易後期維護。所以掌握寫程式碼新增自動佈局約束是非常必要的。原本這篇文章是本系列的第二篇,但是為了提高讀者理解和接受的程度,最終還是把本編放在第三的位置。閒言少敘,我們進入正題。
第一篇文章中講到,每一個佈局約束就是一個明確的線性變化規則

,在數學上是以一次函式的形式表示,即:
y = m * x + c   (公式3.1)
在UIKit中,每一個佈局約束是一個NSLayoutConstraint例項,NSLayoutConstraint類的主要屬性定義如下:

NS_CLASS_AVAILABLE_IOS(6_0)
@interface NSLayoutConstraint : NSObject
...
@property (readonly, assign) id firstItem;
@property (readonly) NSLayoutAttribute firstAttribute;
@property (readonly
) NSLayoutRelation relation; @property (nullable, readonly, assign) id secondItem; @property (readonly) NSLayoutAttribute secondAttribute; @property (readonly) CGFloat multiplier; @property CGFloat constant; ... +(instancetype)constraintWithItem:(id)firstItem attribute:(NSLayoutAttribute)firstAttribute relatedBy:(NSLayoutRelation)relation toItem:(id
)secondItem attribute:(NSLayoutAttribute)secondAttribute multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

其中的firstItem與secondItem分別是介面中受約束的檢視與被參照的檢視。它們不一定非得是兄弟關係或者父子關係,只要它們有著共同的祖先檢視即可,這一點可是autoresizingMask無法做到的。
firstAttribute與secondAttribute分別是firstItem與secondItem的某個佈局屬性(NSLayoutAttribute):

typedef NS_ENUM(NSInteger, NSLayoutAttribute)
{
    NSLayoutAttributeLeft = 1,
    NSLayoutAttributeRight,
    NSLayoutAttributeTop,
    NSLayoutAttributeBottom,
    NSLayoutAttributeLeading,
    NSLayoutAttributeTrailing,
    NSLayoutAttributeWidth,
    NSLayoutAttributeHeight,
    NSLayoutAttributeCenterX,
    NSLayoutAttributeCenterY,
    NSLayoutAttributeBaseline,
    NSLayoutAttributeNotAnAttribute = 0,
    ......//省略剩餘
};

每一個列舉值代表了一個佈局屬性,名字都很直觀,例如Left代表左側,Height代表高度等等。注意,firstItem與secondItem不一定非得是同樣的值,允許定義諸如某檢視的高度等於另一個檢視的寬度這樣的約束(儘管很少這樣做)。NSLayoutAttributeNotAnAttribute這個額外解釋一下,當我們需要為某個檢視指定固定寬度或者高度時,這時候secondItem為nil,secondAttribute為NSLayoutAttributeNotAnAttribute。
relation定義了佈局關係(NSLayoutRelation):

typedef NS_ENUM(NSInteger, NSLayoutRelation)
{
    NSLayoutRelationLessThanOrEqual = -1,
    NSLayoutRelationEqual = 0,
    NSLayoutRelationGreaterThanOrEqual = 1,
};

佈局關係不僅限於相等,還可以是大於等於或者小於等於,這種不等關係在處理UILabel、UIImageView等具有自身內容尺寸的控制元件(自身內容尺寸參見本系列第五篇文章)時非常常用。舉個簡單的例子,UILabel的長度會隨文字的長度而變化,那麼我們可以向UILabel控制元件新增兩個約束,分別是“長度大於等於50”與“長度小於等於200”。這樣,當文字很少時,寬度也至少為50;當文字非常多時,寬度也不會超過200。
multiplier即比例係數。constant即常量。
因此,每個約束就對應如下關係:
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant   (公式3.2)
我們可以呼叫NSLayoutConstraint類的constraintWithItem:…方法,傳入所有需要的引數構造一個新的約束。

理論就到此為止,下面我們還是以第二篇的例子來講解如何使用程式碼新增約束。

縱屏

橫屏

開啟Xcode(8.2.1版),新建Single View Application專案,專案命名為AutoLayoutByConstraint,本文使用Objective-C講解,裝置選擇Universal。下載蘋果Logo圖片apple.jpg,並將其拖入專案中。檔案下載地址:
連結:https://pan.baidu.com/s/1b5AqDo 密碼:e4ff
首先,介面上方用來顯示蘋果Logo圖片的是一個UIImageView,ViewController類的viewDidLoad方法如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];
}

我們需要為logoImageView其新增4個約束:
- logoImageView左側與父檢視左側對齊
- logoImageView右側與父檢視右側對齊
- logoImageView頂部與父檢視頂部對齊
- logoImageView高度為父檢視高度一半
根據公式3.2,在ViewController類的viewDidLoad方法末尾處構造上述4個約束,程式碼如下:

    //logoImageView左側與父檢視左側對齊
    NSLayoutConstraint* leftConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];

    //logoImageView右側與父檢視右側對齊
    NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];

    //logoImageView頂部與父檢視頂部對齊
    NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];

    //logoImageView高度為父檢視高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];

    //iOS 6.0或者7.0呼叫addConstraints
    //[self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];

    //iOS 8.0以後設定active屬性值
    leftConstraint.active = YES;
    rightConstraint.active = YES;
    topConstraint.active = YES;
    heightConstraint.active = YES;

UIView類提供了若干方法和屬性,用於新增或者移除約束。對於iOS 6或者iOS 7可以呼叫addConstraint(s):和removeConstraint(s):方法;對於iOS 8及更新的版本,直接設定約束的active屬性(BOOL值)或者呼叫activateConstraints:與deactivateConstraints:類方法。

就是這麼簡單!現在編譯並執行專案,
縱屏

橫屏
貌似logoImageView的尺寸不太對。如果在viewDidLoad方法中設定self.view的背景色為紅色,看得會更清楚:
logoImageView的尺寸不正確

同時注意到Xcode控制檯打印出了一大段資訊:

2017-02-15 16:44:13.453948 AutoLayoutByConstraint[17260:1271951] [LayoutConstraints] 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. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 

(
    "<NSAutoresizingMaskLayoutConstraint:0x608000086fe0 h=--& v=--& UIImageView:0x7facc44020a0.width == 241   (active)>",
    "<NSLayoutConstraint:0x6000000871c0 H:|-(0)-[UIImageView:0x7facc44020a0]   (active, names: '|':UIView:0x7facc4506110 )>",
    "<NSLayoutConstraint:0x600000087210 UIImageView:0x7facc44020a0.trailing == UIView:0x7facc4506110.trailing   (active)>",
    "<NSLayoutConstraint:0x6080000871c0 'UIView-Encapsulated-Layout-Width' UIView:0x7facc4506110.width == 320   (active)>"
)

//後面省略若干字。。。我簡要翻譯一下:
不能同時滿足約束。或許下列約束中的其中一個是你並不想要的。嘗試如下方法:
(1) 檢查每個約束,試著找出並不期望的約束。
(2) 找到新增該約束的程式碼,並進行修正。
(備註:如果你看到NSAutoresizingMaskLayoutConstraint卻並不理解,請查閱UIview文件中的translatesAutoresizingMaskIntoConstraints屬性。)

看來是出錯了,為什麼會這樣?這是由於自動佈局技術是蘋果在iOS 6當中新加入的,但在那時仍然有很多專案程式碼使用autoresizingMask與setFrame:的方式構建介面。試想,如果將一個已經設定好frame並使用autoresizingMask的檢視新增到一個使用自動佈局的檢視中時,執行時需要隱式地將前者的frame和autoresizingMask轉化為自動佈局約束(這些隱式轉換的約束的型別為NSAutoresizingMaskLayoutConstraint),這樣才能明確其位置與尺寸而不會導致約束的缺失。這個隱式轉換的過程,是由UIView的translatesAutoresizingMaskIntoConstraints屬性的值決定的。預設情況下,為了保證相容性,該值為YES,表示需要自動進行隱式轉換。這對於相容舊的程式碼當然是好的,然而當我們明確為檢視添加了約束後,我們就不希望再進行autoresizingMask的隱式轉換了,否則就會引起約束的衝突。因此,需要特別注意的是,當我們使用程式碼建立檢視時,需要將translatesAutoresizingMaskIntoConstraints屬性的值設定為NO。在viewDidLoad方法中建立logoImageView的程式碼之後,新增如下程式碼:

    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;

再次執行,這次就沒問題了。
到這裡,我想你應該可以把剩餘的檢視和約束的程式碼新增上了,全部程式碼如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

//    self.view.backgroundColor = [UIColor redColor];

    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];

    //logoImageView左側與父檢視左側對齊
    NSLayoutConstraint* leftConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];

    //logoImageView右側與父檢視右側對齊
    NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];

    //logoImageView頂部與父檢視頂部對齊
    NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];

    //logoImageView高度為父檢視高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];

    //iOS 6.0或者7.0呼叫addConstraints
//    [self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];

    //iOS 8.0以後設定active屬性值
    leftConstraint.active = YES;
    rightConstraint.active = YES;
    topConstraint.active = YES;
    heightConstraint.active = YES;

    UIScrollView* scrollView = [UIScrollView new];
    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:scrollView];

    //scrollView左側與父檢視左側對齊
    NSLayoutConstraint* scrollLeftConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];

    //scrollView右側與父檢視右側對齊
    NSLayoutConstraint* scrollRightConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];

    //scrollView底部與父檢視底部對齊
    NSLayoutConstraint* scrollBottomConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];

    //scrollView頂部與logoImageView底部對齊
    NSLayoutConstraint* scrollTopConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];

    scrollLeftConstraint.active = YES;
    scrollRightConstraint.active = YES;
    scrollBottomConstraint.active = YES;
    scrollTopConstraint.active = YES;

    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左側與父檢視左側對齊
    NSLayoutConstraint* nameLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];

    //nameLabel右側與父檢視右側對齊
    NSLayoutConstraint* nameLabelRightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];

    //nameLabel底部與descriptionLabel頂部對齊
    NSLayoutConstraint* nameLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:descriptionLabel attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];

    //nameLabel頂部與父檢視頂部對齊
    NSLayoutConstraint* nameLabelTopConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];

    //nameLabel高度為20
    NSLayoutConstraint* nameLabelHeightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:20.0f];

    nameLabelLeftConstraint.active = YES;
    nameLabelRightConstraint.active = YES;
    nameLabelBottomConstraint.active = YES;
    nameLabelTopConstraint.active = YES;
    nameLabelHeightConstraint.active = YES;

    //descriptionLabel左側與父檢視左側對齊
    NSLayoutConstraint* descriptionLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];

    //descriptionLabel右側與父檢視右側對齊
    NSLayoutConstraint* descriptionLabelRightConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];

    //descriptionLabel底部與父檢視底部對齊
    NSLayoutConstraint* descriptionLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];

    descriptionLabelLeftConstraint.active = YES;
    descriptionLabelRightConstraint.active = YES;
    descriptionLabelBottomConstraint.active = YES;

    //nameLabel寬度與logoImageView寬度相等
    NSLayoutConstraint* nameLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];

    //nameLabel寬度與logoImageView寬度相等
    NSLayoutConstraint* descriptionLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];

    nameLabelWidthConstraint.active = YES;
    descriptionLabelWidthConstraint.active = YES;
}

程式最終專案檔案連結:連結: https://pan.baidu.com/s/1jIdpPK6 密碼: nb3u
自動佈局約束是通過描述檢視間的關係而非強加座標值來進行定位的,它更能滿足不同裝置尺寸的介面佈局,並且更容易讓人理解。雖然上面的程式碼很冗長,但每一句所描述的事實都十分清楚。在此省略自動佈局的好處10000字。。。
區區幾個簡單的檢視,就要寫這麼長的程式碼。。。
。。。
估計你看得有點眼花繚亂了吧,其實我也是修改並檢查了好幾次,又除錯了好幾次才完全寫對的。在下一篇文章中,我將介紹另一種更簡潔的方式,即使用VFL來新增約束,敬請期待吧。