本文為投稿文章,作者:潘晟
盡管現在已經是Apple將Storyboard整合進Xcode中的第四個年頭,大家對於Storyboard的評價仍然褒貶不一。有早期就選擇轉向Storyboard用於UI開發的國內業界領頭人物,也有創建項目就立馬刪除Storyboard的大牛。我經歷過純代碼布局,同時也在多個多人合作項目中使用Storyboard開發界面。在初期繞過各種坑後,Storyboard將會是快速構建UI界面的好幫手,特別是在現如今設備分辨率與尺寸日益增加的情況下,它可以幫助工程師們節約大量的界面代碼書寫時間。Storyboard存在的一大意義在於為UI提供了可視化開發方式,另一方面提供了一種更好的MVC的View層實現方式,讓你的ViewController代碼更簡潔。當然,Storyboard的不足仍然不可忽視,錯誤的難以定位經常讓剛上手的開發者們手足無措,相比於代碼更不容易閱讀的XML源文件所導致多人合作中的沖突不易解決等問題仍然有待完善。本文從各個方面介紹一下Storyboard,分享一下Storyboard的一些使用心得。
歷史
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也是老喬留給我們的眾多禮物之一。
故事板能做什麼
故事板主要為我們提供了以下的功能:(這些功能都是可視化的)
Auto Layout
Size Classes
Secnce的跳轉
代碼可視化
Auto Layout
自動布局顛覆了之前直接操作Frame的布局方式,從思考View應該在哪個位置,變成了考慮在特定條件下,View的所處的位置需要滿足哪些條件。通過這些條件來確定View的Frame。自動布局在實際應用中大體上可以將分為三組:
View與Super View的約束
View自身的約束
View與Other View的約束
假如我們需要在代碼中使用自動布局可以使用 Visual Format Language或者NSLayoutConstraint的簡單工廠方法來生成約束,然後添加到View上。我們來看一個例子:
//用代碼來實現上圖中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和現有設備的對照表如下:
在之前,我們要對橫屏豎屏的界面進行區分,代碼一般是這樣的:
if (IPAD_PORTRAIT) { //TODO:modify something portrait } else { //TODO:modify something landscape }
在Size Classes時代,Apple引入了一個新的類UITraitCollection來封裝水平和垂直方向的Size信息。現在我們通過代碼來改變界面是這樣的:
- (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事件時執行。這一簡單的操作實際上相當於如下代碼:
- (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中重寫如下方法即可:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:[PSViewControllerB description]]) { PSViewControllerB *vc = segue.destinationViewController; //配置..傳值.. } }
手動觸發則需要代碼配合。不同的是,拖線的對象從UIControl變成了UIViewController(不要忘了在Storyboard中填寫Segue Identifier)。
然後在代碼中需要轉場的地方,加上performSegueWithIdentifier:sender:即可。例子如下:
//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
不少設計設都喜歡設計圓角。通常我們需要寫如下代碼:
view.layer.cornerRadius = 5; view.layer.masksToBounds = YES;
為了解決這些重復代碼的問題,有的人喜歡為View寫Category,一行代碼實現圓角。然而這需要在不同的ViewController中不斷引入這個Category,不夠優雅。當然,這種小事情我們也肯定不會願意采用繼承的。
實際上,我們只需要為項目添加一個View的Category,在其中聲明一個@property並加上IBInspectable關鍵字,然後在實現文件中的getter&&setter方法中實現具體的邏輯。不用import頭文件,也不需要運行,Storyboard中將自動出現這個屬性以供配置。這不正是我們夢寐以求的完全解耦嗎!?
//UIView+CornerRadius.h @interface UIView (CornerRadius) @property (nonatomic, assign) IBInspectable CGFloat cornerRadius; @end
//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。然而控制台只會輸出一些模糊其詞的錯誤描述。
*** 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)上,越走越遠。