你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> Storyboard的愛與恨

Storyboard的愛與恨

編輯:IOS開發基礎

54.jpg

本文為投稿文章,作者:潘晟

盡管現在已經是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的約束

QQ截圖20160224113505.png

View自身的約束

QQ截圖20160224113548.png


View與Other View的約束

QQ截圖20160224113613.png

假如我們需要在代碼中使用自動布局可以使用 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中都被濃縮成了兩個按鈕(下圖紅圈中的橢圓按鈕)。

1111.png

  • 紅框1:為被選中View和離他最近的View(可能是SuperView,也可能是另一個同層級的View,看哪個離它更近)添加Leading、Training、Top、Bottom四個屬性約束。

  • 紅框2:為View添加自身寬和高約束

  • 紅色橢圓左側按鈕:當選中多個View時,為多個View添加約束

只需要點擊幾下鼠標,Storyboard就可以幫你輕松完成視圖布局。

Auto Layout Debug

使用代碼來對Auto Layout布局的另一個缺點在於debug的困難。當添加了多余的約束,往往只能在運行時才能發現錯誤。同時,要尋找出是哪一行代碼添加了錯誤的約束也比較費力(往往連控制台都沒有錯誤輸出)。

而Storyboard卻為此提供了非常友好的靜態檢查。主要針對View的約束、布局提供警告和Error,甚至是解決方案。

-----2016-02-19-16-30-51.png

上圖的例子是:我們為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和現有設備的對照表如下:

QQ截圖20160224114047.png

在之前,我們要對橫屏豎屏的界面進行區分,代碼一般是這樣的:

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中定制即可。(如下圖)

QQ截圖20160224114232.png

能不能再給力點兒?

Of Course!除了View,約束也可以不同Size配置不同。最厲害的是,圖片文件也可以根據Size來區分。我們只需要對.xcassets文件勾選Size Classes,就可以為不同Size配置不同圖片.這意味著,在同一個安裝包下,通過Size Classes,我們甚至可以為橫屏iPhone和豎屏iPhone做出完全不同的App!

QQ截圖20160224114320.png

Scene的轉場

如我們所料,Storyboard也可以通過可視化的操作來實現Scene的轉場。

故事板的轉場有兩種,可以分為手動觸發和自動觸發。自動觸發完全由Storyboard實現,而手動觸發則需要配合代碼。前者簡單易用,後者適用於配合業務邏輯,進行不同轉場的觸發。自動觸發的轉場非常簡單,我們只需選擇一個UIControl(比如UIButton),按住Control+左鍵,拖線至目標Scene,選擇Action類型,即可在觸發UIControl的某些事件的時候,自動執行轉場。

ooooo-1.gif

例如利用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)。

aaa.gif

然後在代碼中需要轉場的地方,加上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(僅僅是部分截圖)

-----2016-02-22-17-04-05.png

多Storyboard協作

解決如上問題的方法就是,盡量將項目的界面分割在多個Storyboard文件中。一個最佳實踐是,按照項目功能模塊來區分故事板,例如Login.Storyboard,Chat.Storyboard,Person.Storyboard等。盡量把每個Storyboard的Scene數量控制在20個以內。

同時,Scene間的轉場我們依然可以采用Segue,並且使用起來和單個Storyboard無異。這要多虧Apple在iOS 9新推出的UIStoryboard Reference。

代碼可視化

還有什麼能比代碼可視化更加炫酷的呢?作為前端工程師,最享受的時候,就是枯燥的代碼和算法變成了優美的動畫。但這一切都只在按下command+R之後。

現在,通過Storyboard,我們也可以在編譯時實時預覽我們的代碼所產生的效果。

IB_DESIGNABLE.gif

通過為自定義的View添加IB_DESIGNABLE關鍵字(注意圖中關鍵字的位置),我們讓Storyboard為我們自定義的視圖進行實時渲染。有的人可能會擔心實時渲染造成的性能問題。這點大可放心,Xcode有一套非常優秀的緩存機制(優秀到有些時候必須要clean一下,某些小改動才會在真機上生效),只需要編譯一次,視圖就會被緩存,不會造成每次在Storyboard、代碼文件中切換時多次渲染的問題。

在swift中則為@IBDesignable,放在class關鍵字之前

到這裡令人驚歎的類似Playground的事實渲染功能,已經可以動態地應用在項目中了。我們可以利用IB_DESIGNABLE和IBInspectable來制作圖表等高度自定義的、獨特的視圖。

當然,故事板狂魔對故事板的使用不會就此罷手的,本著一切能用Storyboard配置就不寫代碼的原則,我們也希望可以在故事板中配置自定義控件的屬性。幸運的是,Apple再次為我們的想法提供了可能。

IBInspectable

IBInspectable.gif

通過為自定義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

CornerRadius.gif

實際上,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)上,越走越遠。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved