儘管如今已是Apple將Storyboard整合進Xcode中的第四個年頭,你們對於Storyboard的評價仍然褒貶不一。有早期就選擇轉向Storyboard用於UI開發的國內業界領頭人物,也有建立項目就立馬刪除Storyboard的 大牛。我經歷過純代碼佈局,同時也在多個多人合做項目中使用Storyboard開發界面。在初期繞過各類坑後,Storyboard將會是快速構建UI 界面的好幫手,特別是在現現在設備分辨率與尺寸日益增長的狀況下,它能夠幫助工程師們節約大量的界面代碼書寫時間。Storyboard存在的一大意義在 於爲UI提供了可視化開發方式,另外一方面提供了一種更好的MVC的View層實現方式,讓你的ViewController代碼更簡潔。當 然,Storyboard的不足仍然不可忽視,錯誤的難以定位常常讓剛上手的開發者們手足無措,相比於代碼更不容易閱讀的XML源文件所致使多人合做中的 衝突不易解決等問題仍然有待完善。本文從各個方面介紹一下Storyboard,分享一下Storyboard的一些使用心得。html
歷史前端
1986 年Jean-Marie Hullot發明了IB(Interface Build--Storyboard的前身),而且和Macintosh的工具箱無縫融合,這一工具被Denison Bollay發現了。第二年, Denison Bollay帶着Hullot和他的IB到NeXT,將IB演示給Steve Jobs看。老喬立意識到了IB的價值,並將其歸入到了NeXTSTEP中。以後Steve 帶着NeXT的技術結晶(固然也包括IB)從新迴歸Apple,並將之整合到了Apple的體系中。2008年第一代iPhone SDK發佈的時候,IB就已經捆綁在其中。到了Xcode4,Apple更是直接將其集成進IDE裏。隨後隨着不斷地改進,更新,演變,最終變成了咱們今 天所看到的Storyboard。從某種角度來講,Storyboard也是老喬留給咱們的衆多禮物之一。ios
故事板能作什麼算法
故事板主要爲咱們提供瞭如下的功能:(這些功能都是可視化的)swift
Auto Layout緩存
Size Classes前端工程師
Secnce的跳轉app
代碼可視化ide
Auto Layout工具
自動佈局顛覆了以前直接操做Frame的佈局方式,從思考View應該在哪一個位置,變成了考慮在特定條件下,View的所處的位置須要知足哪些條件。經過這些條件來肯定View的Frame。自動佈局在實際應用中大致上能夠將分爲三組:
View與Super View的約束
View自身的約束
View與Other View的約束
假如咱們須要在代碼中使用自動佈局可使用 Visual Format Language或者NSLayoutConstraint的簡單工廠方法來生成約束,而後添加到View上。咱們來看一個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
//用代碼來實現上圖中View與Super View的約束
UIView *superView = self.view;
UIView *subView = [[UIView alloc] init];
NSLayoutConstraint *leadingConstraint = [NSLayoutConstraint constraintWithItem:superView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:subView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:15];
NSLayoutConstraint *TrailingConstraint = [NSLayoutConstraint constraintWithItem:superView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:subView
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:15];
//topConstraint init...
//bottomConstraint init...
[superView addConstraint:leadingConstraint];
[superView addConstraint:TrailingConstraint];
[superView addConstraint:topConstraint];
[superView addConstraint:bottomConstraint];
// 若是是iOS8+ 則使用下面的方式來激活Constraint
// leadingConstraint.active = YES;
// leadingConstraint.active = YES;
// leadingConstraint.active = YES;
// leadingConstraint.active = YES;
|
是否是一大團亂糟糟的代碼?Visual Format Language用起來更加使人崩潰。好在業界已經有比較好的代碼自動佈局的第三方解決方案。可是仍然會有大堆的簡單界面佈局代碼殘留在你的代碼中。
爲了讓你的生活更輕鬆(也爲了讓代碼更清爽),Storyboard就包含了很是優雅的可視化自動佈局解決方案。以上一切,在Storyboard中都被濃縮成了兩個按鈕(下圖紅圈中的橢圓按鈕)。
紅框1:爲被選中View和離他最近的View(多是SuperView,也多是另外一個同層級的View,看哪一個離它更近)添加Leading、Training、Top、Bottom四個屬性約束。
紅框2:爲View添加自身寬和高約束
紅色橢圓左側按鈕:當選中多個View時,爲多個View添加約束
只須要點擊幾下鼠標,Storyboard就能夠幫你輕鬆完成視圖佈局。
Auto Layout Debug
使用代碼來對Auto Layout佈局的另外一個缺點在於debug的困難。當添加了多餘的約束,每每只能在運行時才能發現錯誤。同時,要尋找出是哪一行代碼添加了錯誤的約束也比較費力(每每連控制檯都沒有錯誤輸出)。
而Storyboard卻爲此提供了很是友好的靜態檢查。主要針對View的約束、佈局提供警告和Error,甚至是解決方案。
上圖的例子是:咱們爲Label添加了多餘的約束,Storyboard用紅色標記出衝突的約束,並給出修改建議:刪除其中一個約束以保證約束的正確性。是否是很友好? :)
Size Classes
Apple 與iOS 8推出了Size Classes的概念。意在解決因設備尺寸形成的適配問題。Size Classes經過將界面的寬度和高度抽象爲正常和緊湊兩種概念,經過合理的組合,能夠將現有設備(以及將來將要出現的設備)劃分到不一樣的Size中。因 此,不管是代碼仍是界面佈局,只須要針對Size進行,而不用再拘泥於分辨是iPhone仍是iPad,是橫屏仍是豎屏的問題了。Size Classes的推出是具備前瞻性的,不管是Apple Watch仍是iOS 9推出的的iPad 分屏模式,均可以用Size Classes完美解決適配的問題。
Size Classes和現有設備的對照表以下:
在以前,咱們要對橫屏豎屏的界面進行區分,代碼通常是這樣的:
1
2
3
4
5
6
7
8
|
if
(IPAD_PORTRAIT)
{
//TODO:modify something portrait
}
else
{
//TODO:modify something landscape
}
|
在Size Classes時代,Apple引入了一個新的類UITraitCollection來封裝水平和垂直方向的Size信息。如今咱們經過代碼來改變界面是這樣的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id )coordinator
{
[
super
willTransitionToTraitCollection:newCollection
withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id context)
{
if
(newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
//To Do: modify something for compact vertical size
}
else
{
//To Do: modify something for other vertical size
}
[self.view setNeedsLayout];
} completion:nil];
}
|
在TODO中作相對應Size的事。
能夠想見的是,仍然會有很是多的佈局代碼佔據着你的源文件。但在Storyboard中,一切變得異常簡單。
使 用Size Classes,咱們只須要選擇相對應的size,在那個Size下進行佈局。運行時,就會根據設備的尺寸,自動地展現相對應Size的佈局。好比 iPhone豎屏就展現width Compact height RegularSize下的信息。當手機橫屏,系統會自動添加一個過渡動畫(雖然有點生硬),並轉到width Regular height Compact的Size。這一切不須要一行代碼。
能不能再給力點?
Sure. 有這麼一種情景:iPhone橫屏下,擁有一個avatarView,豎屏下擁有一個相同的avatar View。這種狀況下咱們只須要在一個Size中完成這個View,而後在Storyboard的attributed inspector中作一些勾選,將其"install"進相對應的Size中,就能夠達到複用的目的。若是有差別,則在對應的Size中定製便可。(如 下圖)
能不能再給力點兒?
Of Course!除了View,約束也能夠不一樣Size配置不一樣。最厲害的是,圖片文件也能夠根據Size來區分。咱們只須要對.xcassets文件勾選 Size Classes,就能夠爲不一樣Size配置不一樣圖片.這意味着,在同一個安裝包下,經過Size Classes,咱們甚至能夠爲橫屏iPhone和豎屏iPhone作出徹底不一樣的App!
Scene的轉場
如咱們所料,Storyboard也能夠經過可視化的操做來實現Scene的轉場。
故 事板的轉場有兩種,能夠分爲手動觸發和自動觸發。自動觸發徹底由Storyboard實現,而手動觸發則須要配合代碼。前者簡單易用,後者適用於配合業務 邏輯,進行不一樣轉場的觸發。自動觸發的轉場很是簡單,咱們只需選擇一個UIControl(好比UIButton),按住Control+左鍵,拖線至目 標Scene,選擇Action類型,便可在觸發UIControl的某些事件的時候,自動執行轉場。
例如利用UIButton轉場,其實是在觸發TouchUpInside事件時執行。這一簡單的操做實際上至關於以下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- (void)viewDidLoad
{
[self.button addTarget:self
action:@selector(showPSViewControllerB)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)showPSViewControllerB
{
PSViewControllerB *viewController = [[PSViewControllerB alloc]init];
//配置..傳值...
[self.navigationController pushViewController:viewController animated:YES];
}
|
Storyboard將Scene轉場變成了可視化的操做又引入了一個新的問題,須要如何傳遞參數給目標ViewController?
解決方法就是,咱們須要在Storyboard中給Segue一個Identifier,而後在源ViewController中重寫以下方法便可:
1
2
3
4
5
6
7
8
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if
([segue.identifier isEqualToString:[PSViewControllerB description]])
{
PSViewControllerB *vc = segue.destinationViewController;
//配置..傳值..
}
}
|
手動觸發則須要代碼配合。不一樣的是,拖線的對象從UIControl變成了UIViewController(不要忘了在Storyboard中填寫Segue Identifier)。
而後在代碼中須要轉場的地方,加上performSegueWithIdentifier:sender:便可。例子以下:
1
2
3
4
5
6
|
//self:PSViewControllerA
if
(isBizSuccess){
[self performSegueWithIdentifier:[PSViewControllerB description] sender:parameter];
}
else
{
[self showTips:@
"some failure reason"
];
}
|
你 能夠利用performSegueWithIdentifier:sender:來進行任何形式的轉場。Segue爲咱們的轉場提供了不一樣的 Action,囊括了常見的UINavigationViewController的push,或者全部ViewController均可以執行的 Modelly Presentation。
事實上,在iOS 8之後,咱們就能夠利用Storyboard結合代碼實現自定義的轉場,不管是在哪種上下文環境中。
採 用Storyboard進行Scene轉場的好處在於,一個ViewController的全部轉場代碼,都集中到了 prepareForSegue:sender: 方法中,debug或者添加新功能時,能夠很容易順藤摸瓜。但缺點一樣明顯。每次轉場的修改/刪除須要 同時修改Storyboard和代碼文件。同時,隨着項目的進行,愈來愈多的Scene和業務邏輯,致使Storyboard中Segue的數量劇增,難 以維護。
巨量的Segue(僅僅是部分截圖)
多Storyboard協做
解 決如上問題的方法就是,儘可能將項目的界面分割在多個Storyboard文件中。一個最佳實踐是,按照項目功能模塊來區分故事板,例如 Login.Storyboard,Chat.Storyboard,Person.Storyboard等。儘可能把每一個Storyboard的 Scene數量控制在20個之內。
同時,Scene間的轉場咱們依然能夠採用Segue,而且使用起來和單個Storyboard無異。這要多虧Apple在iOS 9新推出的UIStoryboard Reference。
代碼可視化
還有什麼能比代碼可視化更加炫酷的呢?做爲前端工程師,最享受的時候,就是枯燥的代碼和算法變成了優美的動畫。但這一切都只在按下command+R以後。
如今,經過Storyboard,咱們也能夠在編譯時實時預覽咱們的代碼所產生的效果。
通 過爲自定義的View添加IB_DESIGNABLE關鍵字(注意圖中關鍵字的位置),咱們讓Storyboard爲咱們自定義的視圖進行實時渲染。有的 人可能會擔憂實時渲染形成的性能問題。這點大可放心,Xcode有一套很是優秀的緩存機制(優秀到有些時候必需要clean一下,某些小改動纔會在真機上 生效),只須要編譯一次,視圖就會被緩存,不會形成每次在Storyboard、代碼文件中切換時屢次渲染的問題。
在swift中則爲@IBDesignable,放在class關鍵字以前
到這裏使人驚歎的相似Playground的事實渲染功能,已經能夠動態地應用在項目中了。咱們能夠利用IB_DESIGNABLE和IBInspectable來製做圖表等高度自定義的、獨特的視圖。
固然,故事板狂魔對故事板的使用不會就此罷手的,本着一切能用Storyboard配置就不寫代碼的原則,咱們也但願能夠在故事板中配置自定義控件的屬性。幸運的是,Apple再次爲咱們的想法提供了可能。
IBInspectable
經過爲自定義View的屬性添加IBInspectable關鍵字(注意圖中關鍵字的位置),咱們能夠將本來須要代碼配置的屬性,放到故事板中。IBInspectable支持如下類型的屬性:
BOOL
NSString
NSNumber
CGPoint
CGSize
CGRect
UIColor
NSRange
UIImage
在swift中則爲@IBInspectable,放在var關鍵字以前
爲系統控件添加IBInspectable
很多設計設都喜歡設計圓角。一般咱們須要寫以下代碼:
1
2
|
view.layer.cornerRadius = 5;
view.layer.masksToBounds = YES;
|
爲了解決這些重複代碼的問題,有的人喜歡爲View寫Category,一行代碼實現圓角。然而這須要在不一樣的ViewController中不斷引入這個Category,不夠優雅。固然,這種小事情咱們也確定不會願意採用繼承的。
實 際上,咱們只須要爲項目添加一個View的Category,在其中聲明一個@property並加上IBInspectable關鍵字,而後在實現文件 中的getter&&setter方法中實現具體的邏輯。不用import頭文件,也不須要運行,Storyboard中將自動出現這個 屬性以供配置。這不正是咱們求之不得的徹底解耦嗎!?
1
2
3
4
5
6
|
//UIView+CornerRadius.h
@interface UIView (CornerRadius)
@property (nonatomic, assign) IBInspectable CGFloat cornerRadius;
@end
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//UIView+CornerRadius.m
@implementation UIView (CornerRadius)
- (void)setCornerRadius:(CGFloat)cornerRadius
{
self.layer.cornerRadius = cornerRadius;
self.layer.masksToBounds = cornerRadius > 0;
}
- (CGFloat)cornerRadius
{
return
self.layer.cornerRadius;
}
@end
|
實際上,IBInspectable是對運行時屬性進行的一種拓展,你在Attributed Inspector中進行的自定義屬性配置,都會在Identity Inspector的運行時屬性中獲得體現。
Storyboard的弊端
Storyboard也並不是十全十美的。它依然有許多的問題亟待解決,有些致命的問題,更是成爲致使許多開發者放棄Storyboard的緣由。在iOS9普及率已經達到77%的今天,Storyboard仍然有不少問題須要完善。
難以維護
Storyboard 在某些角度上,是難以維護的。我所遇到過的實際狀況是,公司一個項目的2.0版本,設計師但願替換原有字體。然而原來項目的每個Label都是採用 Storyboard來定義字體的,所以替換新字體須要在Storyboard中更改每個Label。
幸好咱們知道Storyboard的源文件是XML,最終寫了一個讀取-解析-替換腳原本搞定這件事。
性能瓶頸
當 項目達到必定的規模,即便是高性能的MacBook Pro,在打開Storyboard是也會有3-5秒的讀取時間。不管是隻有幾個Scene的小東西,仍是幾十個Scene的龐然大物,都沒法避免。 Scene越多的文件,打開速度越慢(從另外一個方面說明了分割大故事板的重要性)。
讓人沮喪的是,這個形成卡頓的項目規模並非太難達到。
我猜測是因爲每一次打開都須要進行I/O操做形成的,Apple對這一塊的緩存優化沒有作到位。多是因爲Storyboard佔用了太多內存,難以在內存中進行緩存。Whatever,這個問題老是讓人困擾的。
然而須要指出的是,採用Storyboard開發或採用純代碼開發的App,在真機的運行效率上,並無太大的區別。
錯誤定位困難
Storyboard的初學者應該對此深有體會。排除BAD_EXCUSE錯誤不說,單單是有提示的錯誤,就足以讓人在代碼和Storyboard之間來回摸索,卻沒法找到解決方案。
一個典型的例子是,在代碼中刪除了IBOUTLET屬性或者IBAction方法,可是卻忘了在Storyboard中刪除對應的鏈接,運行後crash。然而控制檯只會輸出一些模糊其詞的錯誤描述。
1
2
3
|
*** Terminating app due to uncaught exception
'NSUnknownKeyException'
,
reason: '[ setValue:forUndefinedKey:]:
this
class is not key value coding-compliant
for
the key drawButton.'
|
有經驗的開發者能夠從drawButton這個關鍵字中找到突破口,但大部分剛接觸Storyboard的開發者,會被困在其中。
最後
綜 合其利弊,毅然選擇了站在Storyboard這邊。一方面是其提供的便利,另外一方面是Apple對Storyboard的大力支持。這一點宏觀上看,可 以在以往對Storyboard的改進和加強上看出,微觀上看,幾乎全部iOS 8以後的simple code都或多或少採用了Storyboard做爲界面開發工具。有理由相信,Storyboard的將來是光明的。
願你們在Storyboard的路(keng)上,越走越遠。