1 更轻量的 View Controllers

介绍 objc.io

objc.io 是关于 Objective-C 最佳实践和先进技术的期刊,欢迎来到第一期!

objc.io 由Chris Eidhof,Daniel EggertFlorian Kugler成立于柏林。我们成立 objc.io 的目的是针对深入的、跟所有 iOS 和 OS X 开发者相关的技术话题创造一个正式的平台。

objc.io 每一期专注于某个特定的主题,包含多篇文章涵盖这个主题的各个方面。第一期的主题是更轻量的 View Controllers,共有 4 篇文章,其中 3 篇来自创始团队,1 篇来自Ricki Gregersen,欢迎他作为我们的第一个特约撰稿人!

从 iOS 应用的代码层面来说,一个常见的问题是 view controllers 难以控制,因为它们做了太多的事。通过重构出可复用的代码,就可以更容易地理解、维护和测试它们。本主题专注于如何让 view controllers 代码保持整洁的最佳实践和技术。

我们将会看到如何使用 view controllers 的协同对象 ( coordinating objects )分离出 view 和 model 的代码,同时将其他控制器对象引入到 view controllers 中。此外,我们还会看到使用 view controller 容器机制来拆分 view controllers。最后,我们会讨论如何测试这些整洁的 view controllers。

在接下来的期刊中,将会有更多出自 Objective-C 社区中优秀的特约撰稿人的文章。Loren BrichterPeter SteinbergerBrent SimmonsOle Begemann已经决定在稍后提交写作。如果你对某个主题有自己的看法,并且想将你的文章贡献给 objc.io,请邮件联系我们吧!

Chris,Daniel,和 Florian。


更轻量的 View Controllers

View controllers 通常是 iOS 项目中最大的文件,并且它们包含了许多不必要的代码。所以 View controllers 中的代码几乎总是复用率最低的。我们将会看到给 view controllers 瘦身的技术,让代码变得可以复用,以及把代码移动到更合适的地方。

你可以在 Github 上获取关于这个问题的示例项目

把 Data Source 和其他 Protocols 分离出来

把UITableViewDataSource的代码提取出来放到一个单独的类中,是为 view controller 瘦身的强大技术之一。当你多做几次,你就能总结出一些模式,并且创建出可复用的类。

举个例,在示例项目中,有个PhotosViewController类,它有以下几个方法:


这些代码基本都是围绕数组做一些事情,更针对地说,是围绕 view controller 所管理的 photos 数组做一些事情。我们可以尝试把数组相关的代码移到单独的类中。我们使用一个 block 来设置 cell,也可以用 delegate 来做这件事,这取决于你的习惯。


现在,你可以把 view controller 中的这 3 个方法去掉了,取而代之,你可以创建一个ArrayDataSource类的实例作为 table view 的 data source。


现在你不用担心把一个 index path 映射到数组中的位置了,每次你想把这个数组显示到一个 table view 中时,你都可以复用这些代码。你也可以实现一些额外的方法,比如tableView:commitEditingStyle:forRowAtIndexPath:,在 table view controllers 之间共享。

这样的好处在于,你可以单独测试这个类,再也不用写第二遍。该原则同样适用于数组之外的其他对象。

在今年我们做的一个应用里面,我们大量使用了 Core Data。我们创建了相似的类,但和之前使用的数组不一样,它用一个 fetched results controller 来获取数据。它实现了所有动画更新、处理 section headers、删除操作等逻辑。你可以创建这个类的实例,然后赋予一个 fetch request 和用来设置 cell 的 block,剩下的它都会处理,不用你操心了。

此外,这种方法也可以扩展到其他 protocols 上面。最明显的一个就是UICollectionViewDataSource。这给了你极大的灵活性;如果,在开发的某个时候,你想用UICollectionView代替UITableView,你几乎不需要对 view controller 作任何修改。你甚至可以让你的 data source 同时支持这两个协议。

将业务逻辑移到 Model 中

下面是 view controller(来自其他项目)中的示例代码,用来查找一个用户的目前的优先事项的列表:


把这些代码移动到User类的 category 中会变得更加清晰,处理之后,在View Controller.m中看起来就是这样:


在User+Extensions.m中:


有些代码不能被轻松地移动到 model 对象中,但明显和 model 代码紧密联系,对于这种情况,我们可以使用一个Store:

创建 Store 类

在我们第一版的示例程序的中,有些代码去加载文件并解析它。下面就是 view controller 中的代码:


但是 view controller 没必要知道这些,所以我们创建了一个Store对象来做这些事。通过分离,我们就可以复用这些代码,单独测试他们,并且让 view controller 保持小巧。Store 对象会关心数据加载、缓存和设置数据栈。它也经常被称为服务层或者仓库

把网络请求逻辑移到 Model 层

和上面的主题相似:不要在 view controller 中做网络请求的逻辑。取而代之,你应该将它们封装到另一个类中。这样,你的 view controller 就可以在之后通过使用带有回调(比如一个 completion 的 block)来请求网络了。这样的好处是,缓存和错误控制也可以在这个类里面完成。

把 View 代码移到 View 层

不应该在 view controller 中构建复杂的 view 层次结构。你可以使用 Interface Builder 或者把 views 封装到一个UIView子类当中。例如,如果你要创建一个选择日期的控件,把它放到一个名为DatePickerView的类中会比把所有的事情都在 view controller 中做好好得多。再一次,这样增加了可复用性并保持了简单。

如果你喜欢 Interface Builder,你也可以在 Interface Builder 中做。有些人认为 IB 只能和 view controllers 一起使用,但事实上你也可以加载单独的 nib 文件到自定义的 view 中。在示例程序中,我们创建了一个PhotoCell.xib,包含了 photo cell 的布局:


就像你看到的那样,我们在 view(我们没有在这个 nib 上使用 File's Owner 对象)上面创建了 properties,然后连接到指定的 subviews。这种技术同样适用于其他自定义的 views。

通讯

其他在 view controllers 中经常发生的事是与其他 view controllers,model,和 views 之间进行通讯。这当然是 controller 应该做的,但我们还是希望以尽可能少的代码来完成它。

关于 view controllers 和 model 对象之间的消息传递,已经有很多阐述得很好的技术(比如 KVO 和 fetched results controllers)。但是 view controllers 之间的消息传递稍微就不是那么清晰了。

当一个 view controller 想把某个状态传递给多个其他 view controllers 时,就会出现这样的问题。较好的做法是把状态放到一个单独的对象里,然后把这个对象传递给其它 view controllers,它们观察和修改这个状态。这样的好处是消息传递都在一个地方(被观察的对象)进行,而且我们也不用纠结嵌套的 delegate 回调。这其实是一个复杂的主题,我们可能在未来用一个完整的话题来讨论这个主题。

总结

我们已经看到一些用来创建更小巧的 view controllers 的技术。我们并不是想把这些技术应用到每一个可能的角落,只是我们有一个目标:写可维护的代码。知道这些模式后,我们就更有可能把那些笨重的 view controllers 变得更整洁。

扩展阅读

View Controller Programming Guide for iOS

Cocoa Core Competencies: Controller Object

Writing high quality view controllers

Stack Overflow: Model View Controller Store

Unburdened View Controllers

Stack Overflow: How to avoid big and clumsyUITableViewControllerson iOS



整洁的 Table View 代码

Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相关代码的整洁和良好组织的技术。

UITableViewController vs. UIViewController

Apple 提供了UITableViewController作为 table views 专属的 view controller 类。Table view controllers 实现了一些非常有用的特性,来帮你避免一遍又一遍地写那些死板的代码!但是话又说回来,table view controller 只限于管理一个全屏展示的 table view。大多数情况下,这就是你想要的,但如果不是,还有其他方法来解决这个问题,就像下面我们展示的那样。

Table View Controllers 的特性

Table view controllers 会在第一次显示 table view 的时候帮你加载其数据。另外,它还会帮你切换 table view 的编辑模式、响应键盘通知、以及一些小任务,比如闪现侧边的滑动提示条和清除选中时的背景色。为了让这些特性生效,当你在子类中覆写类似viewWillAppear:或者viewDidAppear:等事件方法时,需要调用 super 版本。

Table view controllers 相对于标准 view controllers 的一个特别的好处是它支持 Apple 实现的“下拉刷新”。目前,文档中唯一的使用UIRefreshControl的方式就是通过 table view controller ,虽然通过努力在其他地方也能让它工作(见此处),但很可能在下一次 iOS 更新的时候就不行了。

这些要素加一起,为我们提供了大部分 Apple 所定义的标准 table view 交互行为,如果你的应用恰好符合这些标准,那么直接使用 table view controllers 来避免写那些死板的代码是个很好的方法。

Table View Controllers 的限制

Table view controllers 的 view 属性永远都是一个 table view。如果你稍后决定在 table view 旁边显示一些东西(比如一个地图),如果不依赖于那些奇怪的 hacks,估计就没什么办法了。

如果你是用代码或 .xib 文件来定义的界面,那么迁移到一个标准 view controller 将会非常简单。但是如果你使用了 storyboards,那么这个过程要多包含几个步骤。除非重新创建,否则你并不能在 storyboards 中将 table view controller 改成一个标准的 view controller。这意味着你必须将所有内容拷贝到新的 view controller,然后再重新连接一遍。

最后,你需要把迁移后丢失的 table view controller 的特性给补回来。大多数都是viewWillAppear:或viewDidAppear:中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 table view 的editing属性。大多数工作来自重新创建对键盘的支持。

在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:

使用Child View Controllers

和完全抛弃 table view controller 不同,你还可以将它作为 child view controller 添加到其他 view controller 中(关于此话题的文章)。这样,parent view controller 在管理其他的你需要的新加的界面元素的同时,table view controller 还可以继续管理它的 table view。


如果你使用这个解决方案,你就必须在 child view controller 和 parent view controller 之间建立消息传递的渠道。比如,如果用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。


就像你看到的那样,这种结构为 view controller 之间的消息传递带来了额外的开销,但是作为回报,代码封装和分离非常清晰,有更好的复用性。根据实际情况的不同,这既可能让事情变得更简单,也可能会更复杂,需要读者自行斟酌和决定。

分离关注点(Separating Concerns)

当处理 table views 的时候,有许多各种各样的任务,这些任务穿梭于 models,controllers 和 views 之间。为了避免让 view controllers 做所有的事,我们将尽可能地把这些任务划分到合适的地方,这样有利于阅读、维护和测试。

这里描述的技术是文章更轻量的 View Controllers中的概念的延伸,请参考这篇文章来理解如何重构 data source 和 model 的逻辑。结合 table views,我们来具体看看如何在 view controllers 和 views 之间分离关注点。

搭建 Model 对象和 Cells 之间的桥梁

有时我们需要将想显示的 model 层中的数据传到 view 层中去显示。由于我们同时也希望让 model 和 view 之间明确分离,所以通常把这个任务转移到 table view 的 data source 中去处理:


但是这样的代码会让 data source 变得混乱,因为它向 data source 暴露了 cell 的设计。最好分解出来,放到 cell 类的一个 category 中。


有了上述代码后,我们的 data source 方法就变得简单了。


在我们的示例代码中,table view 的 data source 已经分解到单独的类中了,它用一个设置 cell 的 block 来初始化。这时,这个 block 就变得这样简单了:


让 Cells 可复用

有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们可以进一步让 cell 可以复用。首先,我们给 cell 定义一个 protocol,需要用这个 cell 显示的对象必须遵循这个 protocol。然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的任何对象。这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

在 Cell 内部控制 Cell 的状态

如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 cell 修改成我们想要的样子。例如:


然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 cell 自身中去。


总的来说,我们在努力把 view 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单地 API。

控制多个 Cell 类型

如果一个 table view 里面有多种类型的 cell,data source 方法很快就难以控制了。在我们示例程序中,photo details table 有两种不同类型的 cell:一种用于显示几个星,另一种用来显示一个键值对。为了划分处理不同 cell 类型的代码,data source 方法简单地通过判断 cell 的类型,把任务派发给其他指定的方法。


编辑 Table View

Table view 提供了易于使用的编辑特性,允许你对 cell 进行删除或重新排序。这些事件都可以让 table view 的 data source 通过delegate 方法得到通知。因此,通常我们能在这些 delegate 方法中看到对数据的进行修改的操作。

修改数据很明显是属于 model 层的任务。Model 应该为诸如删除或重新排序等操作暴露一个 API,然后我们可以在 data source 方法中调用它。这样,controller 就可以扮演 view 和 model 之间的协调者,而不需要知道 model 层的实现细节。并且还有额外的好处,model 的逻辑也变得更容易测试,因为它不再和 view controllers 的任务混杂在一起了。

总结

Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单地样板代码。

这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单地 API,最终,它变得更加容易理解,也更利于团队协作。

扩展阅读

Blog: Skinnier Controllers using View Categories

Table View Programming Guide

Cocoa Core Competencies: Controller Object



测试 View Controllers

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。

让事情保持简单

测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果你好奇的话,可以去找 Wikipedia 上的文章看看。同时,我们也认为重构和测试可以很好地结合在一起。

测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。

幸好,我们在更轻量的 view controller这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这就说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller这篇文章来获得一些帮助。总的目标就是有清晰的关注点分离。每个类只做一件事,并且做好。这样就可以让你只测试这件事。

记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。

Mocking

当你把一个整体拆分成小零件(比如更小的类)时,我们可以针对每个小的类来进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的mock或stub来绕开它。把mock对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们就可以针对性地测试,并且保证不依赖于应用程序的其他部分。

在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个mock的 table view,这样即使没有 table view,也可以测试 data source,就像下面你即将看到的。起初可能有点难以理解,多看几次后,你就能体会到它的强大和简单。

Objective-C 中有个用来 mocking 的强大工具叫做OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。

本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。

SenTestKit

编者注:   这一节有一些过时了。在 Xcode 5 中 SenTestingKit 已经被 XCTest 完全取代,不过两者使用上没有太多区别,我们可以通过 Xcode 的Edit->Refactor->Convert to XCTest选项来切换到新的测试框架

我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente的 SenTestingKit。这个上古神器从 1997 年起就伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以Tests结尾,它反应了这个类是干什么的。

这些测试类里的方法会做具体的测试工作。方法名必须以test开头来作为触发一个测试运行的条件。还有特殊的-setUp和-tearDown方法,你可以重载它们来设置各个测试。记住,你的测试类就是个类而已:只要对你有帮助,可以按需求在里面加 properties 和辅助方法。

做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的.m文件。通过把类名改成以Tests结尾,类名可以反映出我们在对什么做测试。

编者注: Xcode 5 中 默认的测试模板也不再会自动创建.h文件了

与 Xcode 集成

测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 就会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个-URLForResource:withExtension:方法来方便的使用它。

Xcode 中的每个scheme定义了相应的测试 bundle 是哪个。通过 ⌘-R 运行程序,⌘-U 运行测试。

测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:


编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。最重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得之后再把它们重新全部打开。

还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。

测试 Data Source

好了,让我们开始吧。我们已经通过拆分 view controller 让测试工作变得更轻松了。现在我们要测试ArrayDataSource。首先我们新建一个空的,基本的测试类。我们把接口和实现都放到一个文件里;也没有哪个地方需要包含@interface,放到一个文件会显得更加漂亮和整洁。


这个类没做什么事,只是展示了基本的设置。当我们运行这个测试时,-testNothing方法将会运行。特别地,STAssert宏将会做琐碎的检查。注意,前缀ST源自于SenTestingKit。这些宏和 Xcode 集成,会把失败显示到侧边面板的Issues导航栏中。

第一个测试

我们现在把testNothing替换成一个简单、真正的测试:


实践 Mocking

接着,我们想测试ArrayDataSource实现的方法:


为此,我们创建一个测试方法:

- (void)testCellConfiguration;

首先,创建一个 data source:


注意,configureCellBlock除了存储对象以外什么都没做,这可以让我们可以更简单地测试它。

然后,我们为 table view 创建一个mock 对象

id mockTableView = [OCMockObjectmockForClass:[UITableViewclass]];

Data source 将在传进来的 table view 上调用-dequeueReusableCellWithIdentifier:forIndexPath:方法。我们将告诉 mock object 当它收到这个消息时要做什么。首先创建一个 cell,然后设置mock


第一次看到它可能会觉得有点迷惑。我们在这里所做的,是让 mock记录特定的调用。Mock 不是一个真正的 table view;我们只是假装它是。-expect方法允许我们设置一个 mock,让它知道当这个方法调用时要做什么。

另外,-expect方法也告诉 mock 这个调用必须发生。当我们稍后在 mock 上调用-verify时,如果那个方法没有被调用过,测试就会失败。相应地,-stub方法也用来设置 mock 对象,但它不关心方法是否被调用过。

现在,我们要触发代码运行。我们就调用我们希望测试的方法。


然后我们测试是否一切正常:


STAssert宏测试值的相等性。注意,前两个测试,我们通过比较指针来完成;我们不使用-isEqual:,是因为我们实际希望测试的是result,cell和configuredCell都是同一个对象。第三个测试要用-isEqual:,最后我们调用 mock 的-verify方法。

注意,在示例程序中,我们是这样设置 mock 的:


这是我们测试基类中的一个方便的封装,它会在测试最后自动调用-verify方法。

测试 UITableViewController

下面,我们转向PhotosViewController。它是个UITableViewController的子类,它使用了我们刚才测试过的 data source。View controller 剩下的代码已经相当简单了。

我们想测试点击 cell 后把我们带到详情页面,即一个PhotoViewController的实例被 push 到 navigation controller 里面。我们再次使用 mocking 来让测试尽可能不依赖于其他部分。

首先我们创建一个UINavigationController的 mock:


接下来,我们要使用部分 mocking。我们希望PhotosViewController实例的navigationController返回mockNavController。我们不能直接设置 navigation controller,所以我们简单地用 stub 来替换掉PhotosViewController实例这个方法,让它返回mockNavController就可以了。


现在,任何时候对photosViewController调用-navigationController方法,都会返回mockNavController。这是个强大的技巧,OCMock 就有这样的本领。

接下来,我们要告诉 navigation controller mock 我们调用的期望,即,一个 photo 不为 nil 的 detail view controller。


现在,我们触发 view 加载,并且模拟一行被点击:


最后我们验证 mocks 上期望的方法被调用过:


现在我们有了一个测试,用来测试和 navigation controller 的交互,以及正确 view controller 的创建。

又一次地,我们在示例程序中使用了便捷的方法:

于是,我们不需要记住调用-verify。

进一步探索

就像你从上面看到的那样,部分 mocking非常强大。如果你看看-[PhotosViewController setupTableView]方法的源码,你就会看到它是如何从 app delegate 中取出 model 对象的。


上面的测试依赖于这行代码。打破这种依赖的一种方式是再次使用部分 mocking,让 app delegate 返回预定义的数据,就像这样:


现在,无论何时调用[AppDelegate sharedDelegate].store,它将返回storeMock。将这个技术使用好的话,可以确保让你的测试恰到好处地在保持简单和应对复杂之间找到平衡。

需要记住的事

部分 mock技术将会在 mocks 的存在期间替换并保持被 mocking 的对象,并且一直有效。你可以通过提前调用[aMock stopMocking]来终于这种行为。大多数时候,你希望部分 mock在整个测试期间都保持有效。如果要提前终止,请确保在测试方法最后放置[aMock verify]。否则 ARC 会过早释放这个 mock,这样你就不能-verify了,这不太可能是你想要的结果。

PhotoCell设置在一个 NIB 中,我们可以写一个简单的测试来检查 outlets 设置得是否正确。我们来回顾一下PhotoCell类:


我们的简单测试的实现看上去是这样:

非常基础,但是能出色完成工作。

值得一提的是,当有发生改变时,我们需要同时更新测试以及相应的类或 nib 。这是事实。你需要考虑改变类或者 nib 文件时可能会打破原有的 outlets 连接。如果你用了.xib文件,你可能要注意了,这是经常发生的事。

关于 Class 和 Injection

我们已经从与 Xcode 集成得知,测试 bundle 会注入到应用程序中。省略注入的如何工作的细节(它本身是个巨大的话题),简单地说:注入是把待注入的 bundle(我们的测试 bundle)中的 Objective-C 类添加到运行的应用程序中。这很好,因为这样允许我们运行测试了。

还有一件事会很让人迷惑,那就是如果我们同时把一个类添加到应用程序和测试 bundle中。如果在上面的示例程序中,我们(不小心)把PhotoCell类同时添加到测试 bundle 和应用程序里的话,在测试 bundle 中调用[PhotoCell class]会返回一个不同的指针(你应用程序中的那个类)。于是我们的测试将会失败:

STAssertTrue([cellisMemberOfClass:[PhotoCellclass]], @"");

再一次声明:注入很复杂。你应该确认的是:不要把应用程序中的.m文件添加到测试 target 中。否则你会得到预想不到的行为。

额外的思考

如果你使用一个持续集成 (CI) 的解决方案,让你的测试启动和运行是一个好主意。详细的描述超过了本文的范围。这些脚本通过RunUnitTests脚本触发。还有个TEST_AFTER_BUILD环境变量。

另一种有趣的选择是创建单独的测试 bundle 来自动化性能测试。你可以在测试方法里做任何你想做的。定时调用一些方法并使用STAssert来检查它们是否在特定阈值里面是其中一种选择。

扩展阅读

Test-driven development

OCMock

Xcode Unit Testing Guide

Book: Test Driven Development: By Example

Blog: Quality Coding

Blog: iOS Unit Testing

Blog: Secure Mac Programing




View Controller 容器

在 iOS 5 之前,view controller 容器是 Apple 的特权。实际上,在 view controller 编程指南中还有一段申明,指出你不应该使用它们。Apple 对 view controllers 的总的建议曾经是“一个 view controller 管理一个全屏幕的内容”。这个建议后来被改为“一个 view controller 管理一个自包含的内容单元”。为什么 Apple 不想让我们构建自己的 tab bar controllers 和 navigation controllers?或者更确切地说,这段代码有什么问题:


UIWindow 作为一个应用程序的根视图(root view),是旋转和初始布局消息等事件产生的来源。在上图中,child view controller 的 view 插入到 root view controller 的视图层级中,被排除在这些事件之外了。View 事件方法诸如viewWillAppear:将不会被调用。

在 iOS 5 之前构建自定义的 view controller 容器时,要保存一个 child view controller 的引用,还要手动在 parent view controller 中转发所有 view 事件方法的调用,要做好非常困难。

一个例子

当你还是个孩子,在沙滩上玩时,你父母是否告诉过你,如果不停地用铲子挖,最后会到达美国?我父母就说过,我就做了个叫做Tunnel的 demo 程序来验证这个说法。你可以 clone 这个Github 代码库并运行这个程序,它有助于让你更容易理解示例代码。(剧透:从丹麦西部开始,挖穿地球,你会到达南太平洋的某个地方)


为了寻找对跖点,也称作相反的坐标,将拿着铲子的小孩四处移动,地图会告诉你对应的出口位置在哪里。点击雷达按钮,地图会翻转过来显示位置的名称。

屏幕上有两个 map view controllers。每个都需要控制地图的拖动,标注和更新。翻过来会显示两个新的 view controllers,用来检索地理位置。所有的 view controllers 都包含于一个 parent view controller 中,它持有它们的 views,并保证正确的布局和旋转行为。

Root view controller 有两个 container views。添加它们是为了让布局,以及 child view controllers 的 views 的动画做起来更容易,我们马上就可以看到。


我们实例化了_startMapViewController,用来显示起始位置,并设置了用于标注的图像。

_startMapViewcontroller被添加成 root view controller 的一个 child。这会自动在 child 上调用willMoveToParentViewController:方法。

child 的 view 被添加成 container view 的 subview。

child 被通知到它现在有一个 parent view controller。

用来显示地理位置的 child view controller 被实例化了,但是还没有被插入到任何 view 或 controller 层级中。

布局

Root view controller 定义了两个 container views,它决定了 child view controller 的大小。Child view controllers 不知道会被添加到哪个容器中,因此必须适应大小。


现在,它们就会用 super view 的 bounds 来进行布局。这样增加了 child view controller 的可复用性;如果我们把它 push 到 navigation controller 的栈中,它仍然会正确地布局。

过场动画

Apple 已经针对 view controller 容器做了细致的 API,我们可以构造我们能想到的任何容器场景的动画。Apple 还提供了一个基于 block 的便利方法,来切换屏幕上的两个 controller views。方法transitionFromViewController:toViewController:(...)已经为我们考虑了很多细节。


1, 在开始动画之前,我们把toController作为一个 child 进行添加,并通知fromController它将被移除。如果fromController的 view 是容器 view 层级的一部分,它的viewWillDisappear:方法就会被调用。

2, toController被告知它有一个新的 parent,并且适当的 view 事件方法将被调用。

3, fromController被移除了。

这个为 view controller 过场动画而准备的便捷方法会自动把老的 view controller 换成新的 view controller。然而,如果你想实现自己的过场动画,并且希望一次只显示一个 view,你需要在老的 view 上调用removeFromSuperview,并为新的 view 调用addSubview:。错误的调用次序通常会导致UIViewControllerHierarchyInconsistency警告。例如:在添加 view 之前调用didMoveToParentViewController:就触发这个警告。

为了能使用UIViewAnimationOptionTransitionFlipFromTop动画,我们必须把 children's view 添加到我们的 view containers 里面,而不是 root view controller 的 view。否则动画将导致整个 root view 都翻转。

通信

View controllers 应该是可复用的、自包含的实体。Child view controllers 也不能违背这个经验法则。为了达到目的,parent view controller 应该只关心两个任务:布局 child view controller 的 root view,以及与 child view controller 暴露出来的 API 通信。它绝不应该去直接修改 child view tree 或其他内部状态。

Child view controller 应该包含管理它们自己的 view 树的必要逻辑,而不是把它们看作单纯呆板的 views。这样,就有了更清晰的关注点分离和更好的可复用性。

在示例程序 Tunnel 中,parent view controller 观察了 map view controllers 上的一个叫currentLocation的属性。


当这个属性跟着拿着铲子的小孩的移动而改变时,parent view controller 将新坐标的对跖点传递给另一个地图:


类似地,当你点击雷达按钮,parent view controller 给新的 child view controllers 设置待检索的坐标。


我们想要达到的目标和你选择的手段无关,从 child 到 parent view controller 消息传递的技术,不论是采用 KVO,通知,或者是委托模式,child view controller 都应该独立和可复用。在我们的例子中,我们可以将某个 child view controller 推入到一个 navigation 栈中,它仍然能够通过相同的 API 进行通信。


关于译者:唐天勇

推荐阅读更多精彩内容