尽管如今已是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)上,越走越远。