本節中,會先嗶嗶一下一些可能你們不想看的概念,然後來實現之前我們做的Car Valet程序的橫縱向顯示。
完美縱向顯示
有的iPhone和iPod touch型號具有不同的屏幕高度。自動佈局讓我們能夠創建一組對所有的幾何形狀和大小都起作用的約束。
設計與添加約束是設計和創建用戶體驗(UX)流程中的一部分。首先,設計屏幕模型,通常作爲初始的應用程序規格說明。當開始開發時,不需要使用約束就能進行簡單的初始佈局。這是到此爲止對Car Valet應用程序已經做過的事情。在實踐中,初始設計在實現過程中很容易被修改。因此在界面相當穩定之前最好不要在創建約束上面投入太多時間(畢竟之後還是要做一大堆修改的)。
最後,開始設計與添加約束。當在不同屏幕大小和方向上測試界面時,我們通常會發現一些問題。這會讓我們進入添加約束、嘗試它們、調試、調優,然後添加更多約束的循環。
以約束的方式思考
如果想有效地使用約束,我們需要改變看待界面設計以及佈局的方式。典型的方式是考慮在座標系統中包含視圖元素的矩形。設計活動包括將外觀翻譯爲正確的座標,以及添加更多代碼或額外佈局來調整不同屏幕尺寸和方向的外觀。
藉助約束,可用一種全新的方式來看待界面:屏幕上的可視化元素如何相互關聯?目標是找到的約束(視圖之間的關係)的集合,使得iOS讓視圖能適應任何支持的屏幕大小和方向。這不僅包括一個視圖如何與另一個視圖關聯,還包括如何將試圖分組以及這些組之間如何關聯,這可以包括一個層次結構下的視圖與另一層次結構下的視圖之間的關係。
儘管這看起來很複雜,但關鍵是聚集關係。約束來自那些關係。表示關係的最簡單的方法是使用語言。
在開始設計之前,理解屏幕的約束集合必須滿足兩個條件是很重要的。首先,約束結合在一起,應當針對一種給定的屏幕大小規定一種且僅有一種佈局——也就是說,它們不應當有歧義。其次,約束必須沒有衝突。
對於相互衝突的約束,沒有辦法滿足最高優先級的約束。每個約束的優先級在0到1000之間。1000是默認值,意味着這個約束不許滿足。例如,Previous按鈕不能既有優先級爲1000的固定寬度約束,又有優先級同樣爲1000的自適應內容的寬度約束。降低其中任何一個約束的優先級,或者移除一個約束可以解決此約束衝突。衝突會導致讓應用程序崩潰的運行時異常。
歧義在設計階段更難發現。不像衝突,約束系統可以防止受(歧義)影響的視圖。然而,以不止一種方式來放置它們。歧義出現的典型方式是當改變方向而後變回來時,這時一個或更多個視圖會不在旋轉之前的位置。(之後我們在修改CarValet時會發現的)。
完整的規定
如何知道何時完成佈局?要讓自動佈局無歧義地放置界面中的所有視圖,需要爲每個視圖找到原點和大小。謂詞,每個視圖必須屬於一個或多個乾洗,並規定4個約束:兩個幫助系統設計水平位置和大小,另外兩個計算垂直位置和大小。
OK,正文開始
縱向約束
我們很容易知道,上圖也就是我們的程序,可以分成三部分(不要在意這個圖,我是從之前的文章裏面弄過來的,只是爲了說明),第一部分:“Total Car:999標籤”和“New Car按鈕”;第二部分:中間的分割線;第三部分:剩下的標籤和按鈕;
這時候,我們需要引入三個UIView來分割開來。
首先是頂部
選中一個View
拉到面板中
設置約束
然後把Total Car標籤跟New Car按鈕扔進去,直接用鼠標拖動左側的菜單欄,並將剛加入的View改名改成Add Car Group
這時候如下圖(當然你看到的應該是擠在一起的,下圖是我完成佈局之後的截圖)
同樣的道理,對於分隔符,我們不用引入新的View,因爲它本身就是一個View(忘了的看一下前面的章節在引入分隔符的那部分,我們引入的就是個View,只是把背景顏色給改了)。
把分隔符的名字改成Separator View,然後進行約束
左右兩邊的約束本應該是弄成標準距離,但是我這邊突然爆炸無法選中,用0也是可以的。
弄完之後可能會看到這個
黃線表示他在運行的時候實際上在那個地方,我們可以手動的移動上去
放到黃線位置黃線就沒了。如果出現紅線的話,則表示添加的約束有衝突,這個按上面說的方法自己修改吧。
然後就剩下下半部分,再引入一個View,並將它命名爲View Car Group,在將剩下的標籤按鈕扔進去。
最後左側菜單(IB菜單)應該是這個樣子的(PS,Constraints裏面的數量應該是不一樣的,畢竟我這個是全部完成的樣子,只要Add Car Group、Separator View 和 View Car Group一樣就行)
面板應該是這樣子的(當然,你的面板上半部分跟下半部分View裏面的元素應該是亂成一堆的)
ok,現在呢分好類別了,需要對三個部分的內部元素進行整理。
哦,對了,這裏給不太懂的新手們說明一下。整個故事面板是個View相當於一個容器,或也可以說是一張布(大布),Label跟button就是畫在布上的東西,現在扔進去新的View(小布),在把Label跟button扔進去,相當於我們在另一張布上面畫好Label和button,然後再把這張布貼到之前那張大布上面,相當於完成好小部分再整合到一起。
現在對添加汽車部分進行約束:顯然,Total Car標籤應該放到左上角,而New Car按鈕應該在左下角,這樣看起來纔會好看點(至少我是這麼認爲,當然也可以放到別的地方),選中Total Car標籤,修改如下
對於New Car按鈕的修改類似,我這裏就不貼圖了。注意出現黃色的線的話就更改元素位置或大小,直到黃線消失(不然看的鬧心啊)
對於下半部分,先修改Current Car Number標籤到左上角,Previous到左下角,Next按鈕到右下角。
設置Car Information如圖
設置Edit如圖
現在應該是一樣的了
好了,現在我們縱向的約束已經解決啦,可以自己試着換不同型號的模擬器或拿手機去試試看吧。
橫向約束
有些應用程序僅支持一種屏幕方向。我們現在編寫的是同時支持縱向和橫向屏幕的應用程序。
引用第6節的圖片
橫向之後變成這個樣子。
我們想要的應該是這個樣子或類似的
我們的思路是:
- 在縱向變成橫向的時候,把縱向的約束都去除,然後添加橫向的約束。
- 在橫向變爲縱向的時候,將橫向的約束都去除,然後添加縱向的約束。
我們將用代碼的形式來實現:
首先,我們在代碼中需要獲得縱向屏幕的約束(如果你一開始的應用程序是面對橫向的,那就改成橫向)。
我們之前已經用IBOutlet將連接拖拽到IB對象。雖然我們在這裏也能這樣,但是將很難維護,因爲UI會隨着時間而變化,並且約束會改變。
如果根據所屬視圖將約束分組,會更簡單也更容易維護。我們使用一種特殊的IBOutlet,即IBOutletCollection來實現。這些outlet將多個元素引用蒐集到一個數組中。我們需要爲每個需要刪除約束的視圖準備一個集合。
在代碼中定義IBOutletCollection的通常方式如下:
@property <Optional Property Qualifiers> IBOutletConnection(CollectionElementClass) <Variable Declaration>
第一個可選部分用於聲明屬性限定符,如weak。下一個可選部分是集合的定義,括號中可選的類告訴編譯器集合中可以有什麼類型的元素。當在IB中拖拽出連接時,只讓鏈接到屬於你所設置的類的元素。如果指定了UIView,那麼可以鏈接到任何類型的視圖,包括按鈕、標籤或單純的視圖。但如果指定UIButton,那麼只能鏈接到按鈕。最後一部分聲明瞭實際變量,並且必須是NSArray的某種(類或子類)。
選中一個Add Car Group中的Constraints,如同添加IBOutlet一樣,拉到ViewController.h中
命名爲:addCarViewPortraitConstraints,選擇Outlet Collection
然後在選擇其他的Add Car Group中的Constraints,同樣的方法,不過這時候是拉到剛纔的addCarViewPortraitConstraints的代碼中,如下圖
然後用上面的方法,對於Separator View的Constraints,命名爲separatorViewPortraitConstraints
然後注意了,最後一個是根目錄的Constraints,命名爲rootViewPortraitConstraints
這時候ViewController.h的代碼應該如下:
#import <UIKit/UIKit.h>
#import "CarEditViewControllerProtocol.h"
@interface ViewController : UIViewController
<CarEditViewControllerProtocol>
@property (weak, nonatomic) IBOutlet UILabel *totalCarsLabel;
@property (weak, nonatomic) IBOutlet UILabel *CarNumberLabel;
@property (weak, nonatomic) IBOutlet UILabel *CarInfoLabel;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *addCarViewPortraitConstraints;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *separatorViewPortraitConstraints;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *rootViewPortraitConstraints;
- (IBAction)newCar:(id)sender;
- (IBAction)previousCar:(id)sender;
- (IBAction)nextCar:(id)sender;
@end
我們也可以在下圖位置看到我們進行的約束
或者是右鍵上圖左邊的那個圈圈(截圖截不出來。。。),會出現一個類似右側的菜單。
好了,現在原本的約束已經保存到代碼中。現在我們要進行保存橫向屏幕的約束。
在ViewController.m中,添加代碼,
@implementation ViewController {
NSMutableArray *arrayOfCars; //使用mutable array記錄所有汽車對象
NSInteger displayedCarIndex; //指定靠下位置顯示的汽車的數組索引
NSArray *separatorViewLandscapeConstraints;
NSArray *addCarViewLandscapeConstraints;
NSArray *rootViewLandscapeConstraints;
}
新增的三個變量分別用來保存三部分的橫向約束。
然後我們要把
這三個添加到ViewController.m中,也就是拉。
其中:
- Add Car Group名稱爲addCarView
- Separator名稱爲separatorView
- View Car Group名稱爲viewCarView
ViewController.m中代碼如下:
@implementation ViewController {
NSMutableArray *arrayOfCars; //使用mutable array記錄所有汽車對象
NSInteger displayedCarIndex; //指定靠下位置顯示的汽車的數組索引
NSArray *separatorViewLandscapeConstraints;
NSArray *addCarViewLandscapeConstraints;
NSArray *rootViewLandscapeConstraints;
BOOL isShowingPortrait;
__weak IBOutlet UIView *addCarView;
__weak IBOutlet UIView *separatorView;
__weak IBOutlet UIView *viewCarView;
}
好了,在ViewController.m中創建方法,代碼如下(這是用來添加約束的)
- (void)setupLandscapeConstraints {
NSDictionary *views;//1 創建一個變量綁定字典,用於根據字符串生成約束
id topGuide = self.topLayoutGuide;
id bottomGuide = self.bottomLayoutGuide;
views = NSDictionaryOfVariableBindings(
topGuide,
bottomGuide,
addCarView,
separatorView,
viewCarView
);
NSMutableArray *tempRootViewConstraints = [NSMutableArray new];//2 創建一個臨時可變數組,存放附屬主視圖的生成的約束
NSMutableArray *tempAddViewConstrains=[NSMutableArray new];
NSMutableArray *tempSeparatorViewConstrains=[NSMutableArray new];
NSArray *generatedConstraints;//3創建一個到所有返回的生成約束屬組的可複用引用
addObjectsFromArray:generatedConstraints];//5將生成的約束添加到主視圖約束的臨時數組中。然後生成其餘的主視圖約束,將每個新的集合添加到臨時數組中
generatedConstraints =
[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[topGuide]-[separatorView]-[bottomGuide]"
options:0
metrics:nil
views:views];
[tempRootViewConstraints addObjectsFromArray:generatedConstraints];
generatedConstraints =
[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[topGuide]-[addCarView]-[bottomGuide]"
options:0
metrics:nil
views:views];
[tempRootViewConstraints addObjectsFromArray:generatedConstraints];
//獲得上標題 Current Car Number 和Car info
generatedConstraints =
[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[topGuide]-[viewCarView]-[bottomGuide]"
options:0
metrics:nil
views:views];
[tempRootViewConstraints addObjectsFromArray:generatedConstraints];
//獲得下面的三個按鈕
generatedConstraints =
[NSLayoutConstraint
constraintsWithVisualFormat:@"|-[addCarView]-2-[separatorView]-40-[viewCarView]-|"
options:0
metrics:nil
views:views];
[tempRootViewConstraints addObjectsFromArray:generatedConstraints];
//以下是tempAddViewConstrains的
generatedConstraints=
[NSLayoutConstraint
constraintsWithVisualFormat:@"H:[addCarView(132)]"
options:0
metrics:nil
views:views];
[tempAddViewConstrains addObjectsFromArray:generatedConstraints];
generatedConstraints=
[NSLayoutConstraint
constraintsWithVisualFormat:@"H:[separatorView(2)]"
options:0
metrics:nil
views:views];
[tempSeparatorViewConstrains addObjectsFromArray:generatedConstraints];
//6 將rootViewLandscapeConstraints初始化爲生成約束的可變數組的內容
rootViewLandscapeConstraints = [NSArray arrayWithArray:tempRootViewConstraints];
//7將addCarViewLandscapeConstraints初始化爲包含添加汽車視圖的寬度約束的數組
addCarViewLandscapeConstraints = [NSArray arrayWithArray:tempAddViewConstrains];
//8將separatorViewLandscapeConstraints初始化爲包含分隔符視圖的寬度約束的數組
separatorViewLandscapeConstraints =[NSArray arrayWithArray:tempSeparatorViewConstrains];
}
然後我們要寫一個判斷屏幕方向的函數,以此來使用不同的約束
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) {//1 調用超類方法之後,弄清楚新的屏幕方向。UIInterfaceOrientationIsPortrait是個系統宏,在縱向屏幕時爲true
[self.view removeConstraints:rootViewLandscapeConstraints];//2這是縱向屏幕,因此移除橫向約束,從主視圖開始。對還沒有附屬到視圖的約束調用removeConstraints:也是可行的
[addCarView removeConstraints:addCarViewLandscapeConstraints];
[separatorView removeConstraints:separatorViewLandscapeConstraints];
[self.view addConstraints:self.rootViewPortraitConstraints];//3添加所有的縱向約束。添加已存在的會被忽略
[addCarView addConstraints:self.addCarViewPortraitConstraints];
[separatorView addConstraints:self.separatorViewPortraitConstraints];
} else {//4橫向屏幕
[self.view removeConstraints:self.rootViewPortraitConstraints];//5移除所有特定於縱向屏幕的約束
[addCarView removeConstraints:self.addCarViewPortraitConstraints];
[separatorView removeConstraints:self.separatorViewPortraitConstraints];
[self.view addConstraints:rootViewLandscapeConstraints];//6添加特定於橫向屏幕的約束
[addCarView addConstraints:addCarViewLandscapeConstraints];
[separatorView addConstraints:separatorViewLandscapeConstraints];
}
}
然後讓我們寫的函數調用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
arrayOfCars = [[NSMutableArray alloc] init];//初始化汽車的數組爲空數組
displayedCarIndex = 0;//顯示創建的第一輛汽車
[self setupLandscapeConstraints];
}
這裏來介紹一些可視化約束語言(具體的話自行百度咯),下面表格給出一些語句以及含義。
註釋:
- V表示垂直方向,H表示水平方向 “/”其實是|,但是md會認爲是表格,所以用”/”代替”|”,用的時候要換成”|”
- []中間包含的是視圖名稱
格式字符串 | 組件視圖 | 容器視圖 |
---|---|---|
“V:[topGuide]-[separatorView]-[bottomGuide]” | 主視圖、分隔符視圖 | 主視圖 |
“V:[topGuide]-[addCarView]-[bottomGuide]” | 添加汽車視圖、主視圖 | 主視圖 |
“V:[topGuide]-[viewCarView]-[bottomGuide]” | 主視圖、查看汽車視圖 | 主視圖 |
“/-[addCarView]-2-[separatorView]-40-[viewCarView]-/” | 添加汽車視圖、分隔符視圖、主視圖、查看汽車視圖 | 主(根)視圖 |
“H:[addCarView(132)]” | 添加汽車視圖 | 添加汽車視圖 |
“H:[separatorView(2)]” | 分隔符視圖 | 分隔符視圖 |
解釋:
- H:[addCarView(132)]表示 addCarView視圖水平方向長度爲132
V:[topGuide]-[separatorView]-[bottomGuide]表示SeparatorView視圖豎直方向上連頂端,下連低端
其他的應該是能看懂的
PS:由於我是調試完成之後才寫的博客,所以有些過程的內容就沒掉了。如下圖
這是我們在添加橫向視圖約束的時候會發生的事情,也就是衝突了。這時候需要我們設斷點。可以設置在判斷屏幕旋轉的函數裏面。然後遇到斷點的時候,在輸出面板裏面會有(lldb)這個東西,然後在後面輸入po [[UIWindow keyWindow] _autolayoutTrace]
會出現一些數據,因爲上圖信息中出問題的地方給的是地址碼,所以就根據上面的地址碼找到po [[UIWindow keyWindow] _autolayoutTrace]
後出現的信息裏相應的地方,就大致能知道哪兒個地方的約束衝突了。然後修改即可。
這時候旋轉就能得到我們一開始要的結果了。
PS還沒結束
這時候我們要解決的就是歧義問題了。可以試一下,當我們在縱向屏幕中,點擊Edit,進入到編輯界面,這時候,轉屏,然後返回,你會發現下圖
它竟然沒有轉過來。這是因爲旋轉消息只會被髮送到可見的視圖控制器。當發生旋轉的時候,Add/View場景並沒有顯示。旋轉消息沒有發送,所以約束集合仍然是針對上一次的位置——在當前這種情況下爲縱向。然和縱向的約束不適合橫向的屏幕
所以修改代碼如下
- (void)viewWillAppear:(BOOL)animated {//viewWillAppear:會在ViewController的視圖每次即將在屏幕上顯示時被調用。
[super viewWillAppear:animated];
UIInterfaceOrientation currOrientation = [[UIApplication sharedApplication]statusBarOrientation];//找到當前設備的方向
BOOL currIsPortrait = UIInterfaceOrientationIsPortrait(currOrientation);//當前設備方向是否爲縱向
if((isShowingPortrait && !currIsPortrait) || (!isShowingPortrait && currIsPortrait)){//控制器的上一個方向是否與當前方向不同
[self willAnimateRotationToInterfaceOrientation:currOrientation
duration:0.0f];
}
}
這樣就萬事大吉啦
今天的介紹就到這裏咯
我的另一個博客站點:Arnold-你們好啊