前言
这是斯坦福大学的课程-Developing iOS 9 APPs with Swift,第6节课的内容。主要是讲:组合多个 MVC (Multiple MVCs) 和 View Controller 的生命周期。在 Swift 学习笔记3中我也提到过一些 Multiple MVCs 的基础知识,这篇文章算是应用进阶版。
经过这段时间的学习,个人总结了3个非常好的学习 swift 基础的途径资料(列在下方)。其它的资料比如各路大神的书籍和博客我作为辅助学习,比如喵神,YYKit 的作者 ibireme, 等等。进阶的资料,我觉得学习 Github 中的各种开源项目是个非常好的选择,比如喵神的 Kingfisher 等。当然如果大家能订阅我的博客,我会非常开心哒。:)
下面是我用的最多的3个 Swift 基础学习资料:
- 斯坦福的这个课程,Developing iOS 9 Apps with Swift.
- 苹果官方文档,里面也有实例教程。Learn by doing 永远是最好的学习方法。
- 按住 option 点击代码中的任一单词。这个大家应该都知道,但我是真心越来越觉得这个太好用了。
Demo
又是这张萌蠢的脸,但这次它不仅换了发型,还添加了按钮。感兴趣的童鞋可以去我的 Github 查看源码。
Segue
首先介绍一个名词 - segue。简短的翻译就是:转换。
我们创建了很多 Controller 的 Controller,我们需要用一个 MVC 触发另一个 MVC,这种 MVC 之间的转换就是 segue,一般不用 transition,而用 segue,之后会经常遇到。
主要有4种 segue:
- Show Segue(比如在 Navigation Controller 中,一个 MVC 出现在另一个 MVC 里。)
- Show Detail Segue(比如我们这次的 Demo,一个 master,一个 detail,那么就需要用到这个 show detail。Navigation Controller 中也可以使用 show detail segue,和 show segue 功能一样。)
- Modal Segue(占据整个屏幕)
- Popover Segue(出现一个小弹窗)
Segue 会创建一个新的实例(后文也会继续提到)。就是说每次点击同样的按钮,它返回的不是之前的那个界面,虽然长的也许一样,但其实是一个全新的界面,是重新创建的。
必须要给新建的 segue 一个 Identifier(在 Attributes inspector 中设置)。每个 segue 都要有自己独特的 id,因为我们要对它进行操作。比如我们可以使用 UIViewController 的方法,func performSegueWithIdentifier(identifier: String, sender: Anyobject?)
启用 segue,但我们几乎不用这种方法,一般都在storyboard 中直接用 ctrl 拉线。segue 的 id 更重要的一个用途是:preparing for a segue。用代码来说就是:
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // sender 一般是 button,但可以是任何东西
if let identifier = segue.identifier { // 检查是否 nil
switch identifier {
case "Show Graph":
if let vc = segue.destinationViewController as? GraphController { // 如果非 nil,则作为 GraphController
vc.property1 = ...
vc.CallMethodToSetItUp(...)
}
default: break
}
}
}
在上面的这段代码中,有个 switch case
语句,而我们在 Demo 中,充分发挥了 Swift 的特性,使用了字典,使得我们的代码更加优雅(这也是我为什么喜欢 Swift 的原因,相比于 CPP)。
private let emotionalFaces: Dictionary<String,FacialExpression> = [
注意:这个准备(preparation)的过程是在设置好 outlet 之前完成的,其实也挺符合实际,因为先把该准备的东西准备好了,才能去设置再去呈现。但在实际开发过程中很容易出现 bug,不自觉的就会去使用未设置好的 optional 变量。文末会介绍这样的 bug 以及相应的处理对策。
我们也可以阻止 segue,只要在 UIViewController 中实现下面这个方法,返回 false。
func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool
组合多个 MVC
有3种方式可以将多个 MVC 联合起来,
-
Tab Bar Controller:
每个界面应该是相互独立的,因为如果两个界面相互关联,如上面提到的我们这次的 Demo,我们希望用户点击了某个相应的事件按钮才能看到下个界面,而不是直接点了和事件无关的 Tab Bar 就能看到下个界面。所以我们的 Demo 不能用 Tab Bar Controller。 -
Split Controller:
如我在 Swift 学习笔记3 - Gesture中有提到的, “对于一个split view来说,[0]是主体部分, [1]是细节部分”。对于我们这次的 Demo,带有 “Angry”, "Worried" 等按钮的界面可以是主体部分 (master),而那个呆萌的脸可以作为呈现细节的另一个view (detail)。 别忘记 Split Controller 是用于 ipad 的(以及6 plus),那是因为 6 plus 以前的 iphone 屏幕不够大,没办法 split。要想在 6 里用 split,我们还是需要借助下面的这个哥们,Navigation Controller. -
Navigation Controller:
选中主体部分(master),然后 Editor -> Embed in -> Navigation Controller。Boom!It worked! 其实它就是多了个Back
按钮,这也是 Navigation Controller 精髓所在。在之前的学习笔记中也说过,Navigation Controller 是通过 stack 的方式存放这些 view 的,Back
就代表将现在的这个 view 抛弃掉(是彻底抛弃哦,重新点击按钮,它创建的就是一个新的界面,和之前的那个毛线关系没有啦),然后返回上一个 view。但如果是 Tab Bar,它就是一直存在的,也就是说点击一个 tab A,再点 B,再点 A,它出现的还是原来的 A。用教授的话说就是 “tab bar 不是 segue,navigation 是 segue”。
- 有一点需要注意的是,如果在一个 Navigation Controller 中放有另一个 Navigation Controller,iOS 会自动忽视里面的那个 Navigation Controller。
- 当我们想在 detail 的那个界面加个标题,我们还需要在那个 detail 上 embed 一个 Navigation Controller,但是这样的话在 ipad 上运行那些按钮就没用了,因为 prepare 的是 UINavigationController, 而我们想要 prepare 的是 UINavigationController 里面的内容,也就是我们之前做好的 detail view 里的东西。所以我们还需要加上这段代码,也就是 prepare UINavigationController 里面的内容的。
if let navcon = destinationvc as? UINavigationController {
destinationvc = navcon.visibleViewController ?? destinationvc
}
View Controller 生命周期
生命周期大概是这样的:
- Creation. 一般在 storyboard 中创建实例。
- Preparation if being segued to.
如之前所说,这个准备过程在 Outlet Setting 之前完成。 - Outlet Setting.
- Appearing and disappearing.
- Geometry changes.
- Low-memory situation. 一般在现在的 iphone 中很少发生。但要是发生了,可以释放好久不用的占用很大空间的事件,比如最长未使用原则(LRU)。
在 storyboard 中创建实例,设置了 outlet 之后,就是调用 viewDidLoad 方法了。viewDidLoad, viewWillLoad, viewWillAppear, viewWillDisappear, viewDidAppear, viewDidDisappear 等等这么多方法,我会在下面逐条整理。
- viewDidLoad
view 只会 load 一次,一般在这里在这里进行一些初始化的工作,我们一般不用 init 方法,因为此时 outlet 已经设置完毕了。我们一般也将 update UI 的工作放在这个方法里。但是 view 的 geometry 不在这里设置,因为还不知道使用的设备是什么。这里的 geometry 的意思是 view 的大小尺寸,横向还是纵向之类的。
override func viewDidLoad() {
super.viewDidLoad()
// 进行一些 MVC 初始化的工作
}
- viewWillAppear
这是 view 即将呈现出来之前的方法,一般把需要大量运算的程序放在这里(比如多线程的操作),view 的 geometry 也是在这里设置的,但是如果要做旋转之类的操作,其它地方会响应这些操作。
override func viewDidLoad() {
super.viewDidLoad()
// 进行一些 MVC 初始化的工作
}
- viewDidAppear
这是在 view 呈现出来之后的方法,一些动画在这里进行。
func viewDidAppear(animated: Bool)
- viewWillDisappear
这是 view 即将消失之前的方法,主要做一些简单的清理工作,但一些非常耗时的工作不是在这里进行的,这里可以关闭动画。
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated) // 在所有的 viewWill/Did 方法中,调用 super。
// 进行一些 MVC 初始化的工作
}
- viewDidDisappear
这是在 view 消失之后的方法,释放一些在 willappear 阶段从网络上拿来的数据。
func viewDidDisappear(animated: Bool)
-
func viewWillLayoutSubviews()
和func viewDidLayoutSubviews()
,这两个方法用于几何变换的时候(geometry),而我们在 storyboard 中相对应于那些蓝线设置的 constraints,是在这两个方法之间发生的。这里其实我们不需要做什么特殊操作,因为都是自动完成的。 - viewWillTransitionToSize
在做旋转变换的时候,就是将手机或者 pad 屏幕横过来的时候,我们可以在这里设置一些动画属性。
func viewWillTransitionToSize() {
size: CGSize,
withTransitionCoordinator: UIViewControllerTransitionCoordinator
}
- awakeFromNib
在 preparation 和 outlet set 之前(在 MVC load 之前)。这个会发给任何对象,不只是 ViewController。
概括这个周期,就是:
- Instantiated (一般从 storyboard 中创建实例)
- awakeFromNib
- segue preparation
- outlet set
- viewDidLoad
以下这些会经常调用,比如每次显示和关闭某个 view 的时候 - viewWillAppear & viewDidAppear
- viewWillDisappear & viewDidDisappear
以下几何变换的方法有可能在 viewDidLoad 之后的任何时候被调用 - viewWillLayoutSubviews
- 自动部署布局
- viewDidLayoutSubviews
- didReceiveMemoryWarning (内存不够的时候)
Demo 中的 bug 和对策
有个 bug 在 iOS 开发中应该会经常遇到,就是
fatal error: unexpectedly found nil while unwrapping an Optional value
如 Paul Hegarty 教授所讲,
Your outlets are not set at the time you preparing
这是因为有个 optional 我们没有处理,比如在这个 demo 中,faceView 是 optional 的,faceView 是这样来的:
@IBOutlet weak var faceView: FaceView!
所以在后面 update faceView 的时候,需要考虑它 nil 的情况,有两种措施:
- 第一种方法,以其中一条语句为例
faceView?.eyeBrowTilt = eyeBrowTilts[expression.eyeBrowns] ?? 0.0
注意等号左边的问号 faceView?
,它可以处理 optional nil 的问题,如果式子的任何一个地方为 nil,那么它就会 ignore 整条语句。但是这样的语句很多,我们不能每条都这样处理。仅管我们是码农程序猿,我们也要学会优雅。:)
- 另一个方法就是加上
if faceView != nil { }
,这样就避免了 nil 问题了。
网友评论