WWDC 2015 - 揭开AutoLayout的神秘面纱(Mysteries Of Auto Layout)
在WWDC 2015上关于AutoLayout这堂课上为程序员提供了一些福利般的新特性,并且对于一些技巧性的使用技能,对于程序猿在开发工作中的布局以及布局的调试打了许多鸡血啊,碰巧博主最近的项目也赶上了UI的替换,搞得博主都想现在就在项目中大展手脚的感觉。想想还有点小激动呢。
在WWDC 2015大会上,讲述了12个关于Auto Layout的技能点。如果英文不错的童鞋可以去查看WWDC上的视频: * Mysteries Of Auto Layout Part1 * Mysteries Of Auto Layout Part2
各位童鞋,想知道这十二个技能点么?你特么来求我啊,来求我啊。求我我就告诉你。开始吧,跪下唱征服。
技能点一:Stack View(新)
Stack View
是在iOS 9中推出的一个新的视图,对于程序猿来说是大大滴福利啊,不仅能使xib的可维护性增加而且由于其是轻量级的控件,也能保证程序运行流畅。其实这个视图有点像是Android里面的线性布局,不过它比线性布局好用,主要表现在他的属性方面。其中最常用的属性有4个,具体如下图:
* Axis(或Orientation在 OS X中):用于指明是水平线性布局还是垂直线性布局
* Alignment:用于指定
Stack View
中子视图对其方式
* Distribution:用于指定子视图展示方式
* Spacing:用于指定子视图之间的间距
文字叙述可能有点繁琐,接下来看看苹果官方给我们提供的截图:
通过修改其中的属性可以将
Stack View
中的子视图的布局进行自动排布(如果将线性布局从Horizontal
修改成Vertical
),而不需要通过修改Constraint进行修改布局。
另外在Interface Build
中提供了快速将几个视图添加到Stack View
的快捷按钮,只需通过选中所有的视图,并且点击该按钮即可。
如下图所示:
不仅能方便的将一些视图添加到
Stack View
中,并且还有两个神奇的店,一是在已经建立好的的Stack View
可以插入新的视图,而不需要代码或者布局的调整,Stack View
会自动调整子视图的布局;二是Stack View
中如果将子视图进行隐藏的话,剩下的子视图会自动重新布局,如果加入动画的话,视图也会显示的非常友好和完美。而至于剩下的部分,就由小伙伴们自行探索了,我就不在这里讲解了。不理解的童鞋可以留言。
技能点二:修改约束(Constraints)
在WWDC2015大会上,官方里面是希望我们在修改约束的时候不需要在用Add或者Remove的方法进行修改约束,而是通过NSLayoutConstraint
中的activateConstraints
和deactivateConstraints
的方法进行修改约束,而通过该方法可以使您的程序运行更加流畅。
Note: 在用永远不要去调用
[NSLayoutConstraint deactivateConstraints:self.view.constraints];
方法,因为self.view中的约束不一定都是自己的约束,有可能会包含其他控件的约束。如果调用该方法可能会导致布局错乱。
而对于activateConstraints
和deactivateConstraints
方法来说同样可以添加到动画中,即可以通过Animations来缓慢的进行变化。
下面的做一个Demo试试看吧,首先进行简单的布局,并且先为视图初始化好基础的布局,并创建好对应的contraints。将视图在上方的约束都存在变量_topConstrains
中,而将视图移动到下方的约束都存在_bottomConstrains
,并给ViewController上添加一个点击事件,当点击时则将视图上下移动切换。而切换的代码如下:
/// self.view的点击事件
-(IBAction)kViewClick:(id)sender {
if ([_bottomConstrains.firstObject isActive]) {
[UIView animateWithDuration:3 animations:^{
[NSLayoutConstraint activateConstraints:_topConstrains];
[NSLayoutConstraint deactivateConstraints:_bottomConstrains];
[self.view layoutIfNeeded];
}];
} else {
[UIView animateWithDuration:3 animations:^{
[NSLayoutConstraint deactivateConstraints:_topConstrains];
[NSLayoutConstraint activateConstraints:_bottomConstrains];
[self.view layoutIfNeeded];
}];
}
}
运行效果如下:
技能点三:intrinsicContentSize
对于这个博主暂时想不到这个技能点用什么卵用,不过既然WWDC上提到了,博主就稍微总结一下吧。博主查了一下,对于这个属性是在UIView (UIConstraintBasedLayoutLayering)
中声明的,而根据WWDC上的说法是有一些类的实例(如UILabel
和UIImage
)是有这个属性,这个博主就有点搞不清楚了。不过对于这个属性来说的话,其指的是真实内容的大小,而不是控件的大小,如下图中的UILabel
,控件的大小为蓝色区域的大小,而intrinsicContentSize
则是红色框左右的大小,其大小不跟随空间的大小而进行改变。如果用该大小去指定Label的话其实应该是最适合的大小,Label内部则不会有多余的空间。
另外对于intrinsicContentSize
是由系统产生的,而对于这个属性来说,如果在一些特殊的情况下要使用到这个变量的话(如要重写绘制控件方法时),程序员必须记得要对这个属性进行重写。至于其他的内容博主就不是很懂了,如果有哪位同学对这个属性比较了解其用法的话,麻烦留言给我。小生在此谢过了。
注意:要充分了解该方法所指代的内容,因为之后的技能点五中所提到的
Content Hugging Priority
与Content Compression Resistance Priority
是主要是针对该大小来说明的。
技能点四:自适应的UITableViewCell
对于那些做过空间形式的留言或者是其他要求UITableViewCell的童鞋来说,要根据内容来Cell
高度是多么痛苦的一件事。记得当初笔者做一个类似朋友圈的功能,要做评论点赞图片等合为一体的Cell
,博主就高度这个Boss就整了好久才搞定的,没办法,小生不才,才刚出道。
而在这次的WWDC中,官方提供的技巧中就根据内容来调整Cell
的高度做了优化和讲解,让根据内容来调整高度变得异常的简单。
然后其实在WWDC之前网上已经有了关于如何产生自适应的UITableViewCell
的教程了,不过这次WWDC可能是起到总结的作用。具体的做法如下:
首先先正确适配Cell
中的约束,然后再需要变化的内容又或者说跟需要其内容进行调整高度的空间上(可能是一个或两个)进行添加Top Space to Container Margin
以及Bottom Space to Container Margin
,而这里的Container一般就是UITableViewCell
中的contentView
。添加好约束之后只需在代码中添加tableview.estimatedRowHeight = 60;tableview.rowHeight = UITableViewAutomaticDimension;
这两句代码即可,其中第一句设置Tableview
的Cell的估计高度。而第二句话也是必须的。
如果还是不懂如何进行设置的童鞋可以通过留言或者在微博上@叫什么都不如叫Pluto-Y找我索要Demo的代码。
技能点五:善于用优先级(Priority)
关于优先级,此处所说的优先级主要是针对的是Content Hugging Priority
和Content Compression Resistance Priority
,通过利用这两个优先级可以让控件的大小是否被拉伸或者压缩。
其实从名字来上看就知道了Content Hugging Priority
是保证不被拉伸,而Content Compression Resistance Priority
主要是为了保证其不被压缩,当然这里的拉伸和压缩都是相对于技能点三种的intrinsicContentSize
来说的。简单来说的话,Content Hugging Priority
是用来阻止其变大的,如果用表达式来表示的话,该优先级的作用是用来使view.size <= max(intrinsicContentSize, view.frame.size)
。相反的,Content Compressiong Resistance Priority
是用来阻止其变小的,同样的用表达式来表示的话就是view.size >= min(intrinsicContentSize, view.frame.size)
,值得注意的是view.size指的是通过改变约束后控件的Size,而view.frame.size则是。而经过博主测试后发现如果在Contraints约束下且Priority为默认值的时候,可以分为两种情况:
- 如果
intrinsicContentSize > view.frame.size
的情况下Content Compression Resistance Priority
才会请作用。 - 如果
intrinsicContentSize < view.frame.size
的情况下Content Hugging Priority
才会请作用。
可以查看下图的Demo
情况1:
PS:图中的Label的水平方向上的
Content Hugging Priority = 251
当view.frame.size.width > intrinsicContentSize.width
的情况下,设置Trailing Space
的优先级小于Content Hugging Priority
的情况下会使view.size.width = intrinsicContentSize.width
,而即使Content Compression Resistance Priority
设置成多大都没用。
情况2:
PS:图中的Label在水平方向上的
Content Compression Resistance Priority = 750
当当view.frame.size.width < intrinsicContentSize.width
的情况下,设置Trailing Space
的优先级小于Content Compression Resistance Priority
的情况下会使view.size.width = intrinsicContentSize.width
,而即使Content Hugging Priority
设置成多大都没用。
而在WWDC15上举得例子是另一个,而在我的Demo代码中也有实现。
技能点六:对齐方式(Alignment)
关于对齐方式在WWDC15上主要提到的就是两种方式需要注意,接下来我就用两点来给大家说说吧。
- 关于BaseLine
关于BaseLine来说除了之前系统中就有就有的Baseline
对齐来说,在iOS9中还添加了firstBaseline
和lastBaseline
的对齐,而这两个的对齐方式主要是针对文本的对其,例如Label、Button之类的控件。而想要知道这之间又是面区别的话,就看下面的两个简单的Demo:
第一个Demo是让一个Label的firstBaseline等于Button的firstBaseline,具体代码和效果图如下:
//_kBaseLineLabel是左边的Label,_kBaseLineBtn是右边的Button
NSLayoutConstraint *kContrains = [_kBaseLineLabel.firstBaselineAnchor constraintEqualToAnchor:_kBaseLineBtn.firstBaselineAnchor];
[NSLayoutConstraint activateConstraints:@[kContrains]];
效果图如下:
效果就是Label的第一行文字的下端与Button文字的下端对其。
第二个Demo是让Label的lastBaseline等于Button的lastBaseline,具体代码和效果图如下:
//_kBaseLineLabel是左边的Label,_kBaseLineBtn是右边的Button
NSLayoutConstraint *kContrains = [_kBaseLineLabel.lastBaselineAnchor constraintEqualToAnchor:_kBaseLineBtn.lastBaselineAnchor];
[NSLayoutConstraint activateConstraints:@[kContrains]];
效果图如下:
效果就是Label的最后一行文字的下端与Button文字的下端对其。而不管Label怎么变化,都会保证这两个控件文字的下端对其。这个在很多场景中都能用到。
- 关于Leading和Trailing
关于这点很多人都会觉得Leading就是左对齐,而Trailing就是右对齐。这也是为什么博主不直接下左对齐右对齐而是写Leading和Trailing的意思。对于中文中当然Leading可以认为是左对齐,同理Trailing。毕竟中文是从左往右读的习惯。然后对于一些国家来说,他们的习惯是从右往左读,那么这么来说的话Leading就是右对齐,而Trailing就是左对齐了。所以WWDC中建议一定要用Leading和Trailing而不是用左对齐右对齐。当然在国内也有人用前边后边来表示Leading和Trailing,不过博主还是觉得没有Leading和Trailing来说更加能体现其中的精髓。
到此为止Part1就好了。接下来就准备开始说Part2的内容了。
技能点七:Layout的周期
关于布局周期这里主要是针对修改布局的情况下,其实主要就分为以下三个步骤,并且这三个步骤之间形成一个循环。具体如下:
先是约束变化,然后会产生一个延迟的布局事件,最后到达App的循环。而其中App的循环又会继续等待约束变化的过程,从而产生一个新的周期。
约束变化
针对这里的约束变化主要包括以下几种:
- Activating和Deactivating约束
- 设置约束的constant或者priority
- 添加或者删除视图
以上这几种的约束都会产生一个新的布局周期。会根据以下的变化重新计算布局,Layout的引擎则会根据计算的结果接收到新的布局变量,而视图就会自动去调用
superview.setNeedsLayout()
。这些步骤就组成了约束变化的过程。
延迟的布局事件
延迟布局的通过主要包括以下几个步骤:
- 重新放置那些错放的视图
- 更新约束并且重新给视图的frame赋值
当然除了约束变化会导致延迟布局的事件以外,还可以通过setNeedsUpdateConstraints()
来触发布局事件。
并且在苹果官网还是强烈建议使用Activating Contraints
和Deactivating Contraints
而不是去Change Contraints
(即Add 或者Remove约束),毕竟前者比后者来说说速度快多了。小伙伴们以后要习惯用Activating 和 Deactivating来修改约束。
关于layoutSubviews
(在Mac OX中叫做layout
)的方法的作用主要在于让布局引擎接受到新的布局的约束值,而不是重新排布各个视图的位置。并且将新的值设定到各个子视图中(在iOS中用的是setBounds
和setCenter
在Mac OX中用的是setFrame
)
而如果在重写layoutSubviews
的过程中需要注意以下几点:
- 必须调用
[super layoutSubviews]
的方法 - 修改视图的布局需要在调用
[super layoutSubviews]
之前进行修改 - 别调用
setNeedsUpdateConstraints
- 别修改不是属于该视图的子视图
- 别随意的修改约束
然而这个技能点主要是讲解了一些理论上的知识,而博主对此也不是有非常深刻的理解,所以可能讲解的有些不是特别好,望大家见谅。不过一些注意点是大家都需要注意的,麻烦大家记一下。
技能点八:与之前的适配进行交互
对于此来说,iOS关于布局最先是通过setFrame
来进行布局,之后在此基础上添加了一个autoresizingMask
来进行根据父视图的修改来调整子视图的布局,最后在近几个版本中添加了一个Auto Layout
的布局机制,主要是通过约束来进行布局。而技能点八主要是针对那些就的布局机制与AutoLayout共存情况下的一些情况的注意点。
当然在Auto Layout下,小伙伴们依然可以用setFrame
来进行布局,但是在之后可能会有个延迟布局的事件来回调的情况下,可能您的setFrame
会出现失效的情况。然后对于那些小伙伴们真的希望通过setFrame
来进行布局的控件来说苹果有提供一个变量进行处理该种情况。这个变量就是translatesAutoresizingMaskIntoConstraints
,如果在创建某个视图是通过IB的情况下,它的默认值就是NO
,而如果在创建某个视图是通过代码的情况下,它的默认值则就是YES
。而如果小伙伴们想在代码中添加或声明约束给代码生产的视图的话,即的将这个变量设置为NO,否会出现报错的情况,如下图的情况下:
其实只需要将translatesAutoresizingMaskIntoConstraints
设置成NO
即可。正确的代码如下:
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
//别忘了将这个变量设置成NO
btn.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:btn];
NSLayoutConstraint *kTopContraints = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:10];
NSLayoutConstraint *kLeadingContraints = [NSLayoutConstraint constraintWithItem:btn attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:10];
[NSLayoutConstraint activateConstraints:@[kTopContraints, kLeadingContraints]];
然而其实将这个变量设置成YES
后,对于程序来说,可以做以下操作:
- 任意的通过
setFrame
进行布局 - 可以用
autoresizingMask
来实现约束 - 其他的视图可以与该视图建立约束
技能点九:约束的创建
关于约束的创建来说(针对代码创建),其实在iOS 9 中主要是优化了之前方法的可读性。简化了之前繁琐的创建方法,使方法更加简洁与可读。废话不多说,上代码。 对于以前来说要创建约束需要用一下的方法:
[NSLayoutConstraint constraintWithItem:self.saveButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:100];
[NSLayoutConstraint constraintWithItem:self.saveButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:100];
其实从代码层面上来说,这种方式过于复杂与繁琐。而在新的sdk中添加了简化的方法,如下:
[self.saveButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:100];
[self.saveButton.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100];
相对上面的方法来说一目了然。当然在iOS9中添加了很多Anchor的属性,大部分针对NSLayoutAttribute
都有其对应的属性。
然而对于这种新的SDK来说也有一些需要注意的地方:
- 不要给位置的
Anchor
设置成一个常量 如:[v1.leadingAnchor constraintEqualToConstant:100];这种做法是不允许的。 - 不要给位置的
Anchor
设置成一个Size的Anchor
如: [v1.leadingAnchor constraintEqualToAnchor:v2.widthAnchor];同样是不被允许的。
技能点十:Layout Guide
首先先举一个例子,例如在一个场景下,我们需要3个Button,而三个Button中间的距离需要保持相等的距离。而在以往的情况下需要在Button中间建立View并且用约束来保证Button与Button之间的距离保持一致。如下图所示:
然后其实对于中间的两个View来说,其实这两个View是完全没有实际的意义,只是为了保证Button与Button之间的距离是一致的作用。
而对于新的SDK中添加了新的类用于处理该功能,即UILayoutGuide(Mac OX中为NSLayoutGuide)。UILayoutGuide在官方文档中说了,他是定义了一个正方形的区域用于Auto Layout的机制。其用法也相当简单,可以将其看做上图中的无意义的视图。并且其也拥有跟视图一样的约束的操作。或许我们就可以将UILayoutGuide看做是一个特殊的视图即可。
具体实现代码如下:(博主用Save、Cancel和Clear三个按钮用来替换)
UILayoutGuide *space1 = [[UILayoutGuide alloc] init];
[self.view addLayoutGuide:space1];
UILayoutGuide *space2 = [[UILayoutGuide alloc] init];
[self.view addLayoutGuide:space2];
[space1.widthAnchor constraintEqualToAnchor:space2.widthAnchor].active = YES;
[self.saveButton.trailingAnchor constraintEqualToAnchor:space1.leadingAnchor].active = YES;
[self.cancelButton.leadingAnchor constraintEqualToAnchor:space1.trailingAnchor].active = YES;
[self.cancelButton.trailingAnchor constraintEqualToAnchor:space2.leadingAnchor].active = YES;
[self.clearButton.leadingAnchor constraintEqualToAnchor:space2.trailingAnchor].active = YES;
具体效果如下:(左图为IB中的图,右图为模拟器的运行效果)
技能点十一:布局的调试技巧
关于布局的调试功能,博主就准备用于官方类似的Demo进行讲解。具体如下截图的布局:
然而在该例子中由于配置不当会导致运行是出现Layout的错误,而利用该问题中导致的错误来说如何调试布局中出现的问题。而在运行过程中导致的问题的输出如下:
2015-10-13 00:57:46.025 WWDC15AutoLayoutMysteries[13103:4654603] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7fb999710a70 H:[animalImg]-(122)-| (Names: animalImg:0x7fb999659790, '|':UIView:0x7fb999659630 )>",
"<NSLayoutConstraint:0x7fb999710ac0 H:|-(123)-[animalImg] (Names: animalImg:0x7fb999659790, '|':UIView:0x7fb999659630 )>",
"<NSLayoutConstraint:0x7fb999659950 'imgAspectRatio' animalImg.width == 1.5*animalImg.height (Names: animalImg:0x7fb999659790 )>",
"<NSLayoutConstraint:0x7fb999710cf0 'imgTopMargin' V:|-(21)-[animalImg] (Names: animalImg:0x7fb999659790, '|':UIView:0x7fb999659630 )>",
"<NSLayoutConstraint:0x7fb999710d60 'labelTopMargin' V:|-(100)-[questionLbl] (Names: questionLbl:0x7fb99965ec60, '|':UIView:0x7fb999659630 )>",
"<NSLayoutConstraint:0x7fb999710dd0 'labelTopToImg' V:[animalImg]-(8)-[questionLbl] (Names: questionLbl:0x7fb99965ec60, animalImg:0x7fb999659790 )>",
"<NSLayoutConstraint:0x7fb999587c60 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7fb999659630(320)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fb999659950 'imgAspectRatio' animalImg.width == 1.5*animalImg.height (Names: animalImg:0x7fb999659790 )>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
对于大部分人来说看到这么多的输出马上会感到害怕,甚至会不假思索的马上将结果复制到百度或者google中去搜索,希望直接从别人的历史记录中得到答案,然而本王想说的是,不要害怕,让本王告诉你如何快速定位问题。
遇到这种问题,首先先去确定该问题是不是由于前面所说的translatesAutoresizingMaskIntoConstraints
没有设置成正确的值所导致的,具体设置查看上面的教程,毕竟很多情况下有人会忘记将其设置成正确的值。
如果在确认了不是由于上述问题所导致的话,接下来就直接去查看Log的最后一句,即Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fb999659950 'imgAspectRatio' animalImg.width == 1.5*animalImg.height (Names: animalImg:0x7fb999659790 )>
,可以看出肯定是由于哪个约束与这个约束产生了冲突而导致。接下来就对Log中的每个约束进行绘制到纸上,查看可能哪个约束与其冲突。这里就采用一下官方PDF中的截图,与Demo中的布局类似,大家可以借鉴一下。
可以看得出来Label的TopMargin与ImgTopMargin+ImgHeight+ImgToLabelMargin的之间会产生冲突,而其实应该将Label与父视图的上边距应该进行去除。
然而如果是细心的朋友可以看到在上面的输出语句中可以看到animalImg
、labelTopMargin
、imgAspectRatio
等一些一看就知起意的命名,而如果正常情况下的话应该不会出现有这些命名的存在。因此这里需要提一个技巧,即给对应的视图和约束设置identifier,而这些identifier则会在出现错误输出更友好的输出,保证程序猿们更好的读懂错误的Log。
而对于identifier在IB上也可以设置,也可以通过identifier的属性进行设置。官方推荐给视图、约束以及LayoutGuide设置identifier。而对于在VFL的约束,则需要通过遍历来设置identifier。
同时如果在约束过多的情况下,可以通过constraintsAffectingLayoutForAxis
(Max OX中方法名为constraintsAffectingLayoutForOrientation
)该方法来进行过滤,该方法是用来显示一个方向的约束。0
则显示水平方向的约束,而1
的情况下显示垂直方向的约束。看看我们的苹果是如此的贴心。
这里就总结一下,主要的技巧包括以下几个:
- 如果出现错误的Log,则从Log的底部进行开始读
- 确认
translatesAutoresizingMaskIntoConstraints
的变量是否设置正确 - 给所有的视图、约束以及LayoutGuide的设置对应的identifier,方便之后出现错误日志是方便阅读
- 利用
constraintsAffectingLayoutForAxis
进行过滤过多的约束
技能点十二:处理模糊的布局
关于出现模糊的布局的可能情况主要包括以下几种:
- 约束不足或者可以说是缺少约束
- 优先级的冲突,即可能都在使用默认的优先级(而关于其中的优先级的详细内容可以查看上面的技能点)
而如果想要解决布局的模糊可以通过一下的工具进行处理:
- 在IB中出现红色和黄色的图标,即出现布局上的Error和Warmming
- 使用
_autolayoutTrace
方法进行查看视图的视图是否有Ambigous的Layout,即是否出现模糊的布局,该方法需要在LLDB中进行使用 - 而其中最后的方法则是通过菜单中的
Debug->ViewDebug->Show Alignment Rectangles
来进行查看视图 - 其中在设置断点的情况,请断点暂停了,则可以通过
按钮来进行查看视图的层次
- 除了上述的
_autolayoutTrace
方法以外,还有另一个方法exerciseAmbiguityInLayout
进行多次运行则可看到由于模糊的情况下Layout Engine可以给我们提供的多种情况中来判断是由于什么原因到只的视图模糊
关于调试的细节这里就希望各位童鞋自己去多尝试尝试了,如果有什么问题的话可以通过留言、邮件以及通过@叫什么都不如叫Pluto-Y来进行提问。博主会尽其所能为您解答。
Github代码的地址为:https://github.com/Pluto-Y/WWDC15Demos,有需要的同学可以前往下载查看。不懂的可以留言。