在过去的几个月,我有机会和很多不同项目领域的开发者们交流,交谈的结果是,甚至一些最有经验的开发者有时也会犹豫如何组织一个合适的视图控制器层次结构ViewController hierarchy

精巧的设计一个好的导航模式navigation pattern是工程项目中极为重要的一部分,如果不脚下留神的话,你将会很容易的掉进一个错综复杂的、不可控的、耦合性连接的迷宫里去-噩梦啊。很明显,你可以选择一个类似UINavigationController或者UITabBarController的标准的解决方案,但是有的时候这些都太简单,不够用。
随着iOS5的推出,苹果增加了一个值得被注意的特征,但看起来并没有太多开发者意识到:只要按照苹果建议的几条规则,你就能够通过创建一个自定义视图控制器容器,来创建一个更好更合适的导航。
让我们通过创建一个简单的应用来深入讨论下这个话题。

控制器工厂

在这个空想出来基本无用的程序里,我们创建一个有无数控制器的导航,这些控制器能通过点击按钮来进行更新。

下面的这个简单的原理图能帮助我们理解它是怎么工作的。
image

容器Container:这是一个UIViewController的子类,它有一个叫做detailView的子视图来作为其他控制器视图的容器,有一个按钮来呈现一个新的DetailController。当前DetailController的引用用currentDetailController的属性来存储。

这是我们的 自定义控制器容器(Custom Container Controller),它的主要责任是明确怎么给用户呈现他的子元素(即容器内的控制器)

详情Detail:这是一个简单的UIViewController,包含一个Label和一个UIGestureRecognizer。在它的视图上面滑动就可以随即改变Label的颜色。这样一个类,它的实例将会作为容器控制器的子元素。这个控制器的视图将会依附与容器的detaiView,但是它依然受DetailController管理,接收手势事件。

在文章的末尾你会找到工程的源码,下载之后接着进行下面的步骤。

添加容器的子控制器

在继续之前,先给Jack Flintermann表示致意,他是FlatUIKit-一个能让标准UIKit组件变的更加强大的相当酷的库的作者。为了让这个例子更有型,我使用了这里面的一些类。

打开文件 ContainerViewController,看一眼函数 presentDetailController。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)presentDetailController:(UIViewController*)detailVC{

//0. Remove the current Detail View Controller showed
if(self.currentDetailViewController){
[self removeCurrentDetailViewController];
}

//1. Add the detail controller as child of the container
[self addChildViewController:detailVC];

//2. Define the detail controller's view size
detailVC.view.frame = [self frameForDetailController];

//3. Add the Detail controller's view to the Container's detail view and save a reference to the detail View Controller
[self.detailView addSubview:detailVC.view];
self.currentDetailViewController = detailVC;

//4. Complete the add flow calling the function didMoveToParentViewController
[detailVC didMoveToParentViewController:self];

}
  • step 0:
    step 0的注释后面再解释,实际上我们就是把当前显示的DetailController从层级结构中移除。

  • step1:
    函数 addChildViewController是iOS5之后为自定义容器新加的函数集合中的一部分。就这么简单,我们就把一个新的DetailController作为子元素添加到了控制器容器中。

  • step2:
    新的Detail Controller的视图将要与之前定义的容器中的视图绑定。我们可以先检查一下它的尺寸(Frame),如果需要可以改变。

  • step3:
    将Detail的视图和容器的detailView进行绑定,然后把新的Detail Controller作为当前Detail Controller

  • step4:
    函数 didMoveToParentViewController 也是UIVieController类新增加的。多亏有这个函数,我们可以给Detail Coontroller对象发送消息来确定已经是另一个控制器的子控制器了

移除容器中的子控制器

现在,在相同的文件中,检查函数 -(void) removeCurrentDetailViewController

  • Step1
    我们给当前的Detail controller发送消息:willMoveToParentViewController,参数为nil,通知大家它将要从它的父层级结构中移除。这条消息也是iOS新加的函数集中的一部分。

  • Step2
    将当前DetailController的视图从其父视图中移除

  • Step3
    调用标准函数 removeFromParentViewController ,我们就把当前的Detail Controller从容器中移除了。
    当这个函数被调用的时候,系统会自动给Detail Controller发送一条参数为nil的消息:didMoveToParentViewController

结论

第一个DetailController是在控制器容器里面的 viewDidLoad 函数里创建的。方法 initWithString:withColor 能够帮助我们创建一个 DetailViewController 的实例,借助于 presentDetailController 我们可以把这个实例添加到当前DetailController中

上面已经提到,由你来决定什么时候点击容器控制器主视图里的按钮来呈现一个新的DetailController。这个按钮和 addDetailController 连接在一起,它的代码相当简单:

1
2
3
4
5
6
7
8
9
- (IBAction)addDetailController:(id)sender {
DetailViewController *detailVC = [[DetailViewController alloc]initWithString:@"This is a new viewController!" withColor:[UIColor asbestosColor]];

/* Mode 1 */
[self presentDetailController:detailVC];

/* Mode 2 */
//[self swapCurrentControllerWith:detailVC];
}

实质上,它创建了一个新的DetailViewController,然后调用了函数 presentDetailController

你可以编译运行,点击按钮“New Controller”来创建一个新的DetailController,在视图上面滑动来改变标签的颜色

增加过渡效果

控制器的切换我们还完全没有注意到。通过创建一组动画来让新控制器的推送变的更加清晰是一个不错的主意。

不用两个独立的增加和移除DetailController方法,我们把所有的操作都放在一个单独的函数中,我们叫它 swapCurrentControllWith:让要推送的控制器作为输入参数。
下面是完整的代码和注释:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
- (void)swapCurrentControllerWith:(UIViewController*)viewController{

//1. The current controller is going to be removed
[self.currentDetailViewController willMoveToParentViewController:nil];

//2. The new controller is a new child of the container
[self addChildViewController:viewController];

//3. Setup the new controller's frame depending on the animation you want to obtain
viewController.view.frame = CGRectMake(0, 2000, viewController.view.frame.size.width, viewController.view.frame.size.height);

//The transition automatically removes the old view from the superview and attaches the new controller's view as child of the
//container controller's view

//Save the button position... we'll need it later
CGPoint buttonCenter = self.button.center;

[self transitionFromViewController:self.currentDetailViewController toViewController:viewController
duration:1.3 options:0
animations:^{

//The new controller's view is going to take the position of the current controller's view
viewController.view.frame = self.currentDetailViewController.view.frame;

//The current controller's view will be moved outside the window
self.currentDetailViewController.view.frame = CGRectMake(0,
-2000,
self.currentDetailViewController.view.frame.size.width,
self.currentDetailViewController.view.frame.size.width);

self.button.center = CGPointMake(buttonCenter.x,1000);


} completion:^(BOOL finished) {
//Remove the old view controller
[self.currentDetailViewController removeFromParentViewController];

//Set the new view controller as current
self.currentDetailViewController = viewController;
[self.currentDetailViewController didMoveToParentViewController:self];

//reset the button position
[UIView animateWithDuration:0.5 animations:^{
self.button.center = buttonCenter;
}];

}];
}

我们把“present”和“remove”两个动作放在了上面提到的同一个函数里,在这个函数里,使用函数 animateWithDuration:animations:completion 我们可以使用一些简单的动画来交换当前控制器的视图和新控制器的视图。

(为了看到结果,你得把函数 addDetailController 中的Mode2的注释去掉,然后再注释掉Mode1)

总结

下面是一个简单的流程图来重述一下添加和移除一个子控制器的步骤。这个几乎是所有以后要创建的导航模式的基本步骤。

  • Current detail controller: [current willMoveToParentViewController:nil]
  • Next detail controller:[container addChildViewController:next]
  • Next detail controller: [next willMoveToParentViewController:self] (Automatically called by point 2)
  • Next detail controller:[container.view addSubview:next.view]
  • Current detail controller: [current.view removeFromSuperView]
  • Current detail controller: [current removeFromParentViewController]
  • Current detail controller:[current didMoveToParentViewController:nil] (automatically called by point 5)
  • Next detail controller: [next didMoveToParentViewController:self]

使用这些“新”函数,你可以创建任何一个导航模式。UINavigationController和UITabBarController很明显是相当的控制器容器的例子,他们有不同的逻辑和用法,但是在最终是在同一时刻给用户呈现一个控制器这一点上是一致的。

另一个容器控制器的例子是 UISplitViewController,它可以在同一时刻呈现两个控制器

简单的说,你的自定义容器本质上就是负责明确哪一个控制器应该被呈现,用户怎么样才能在控制器之间进行切换。

我喜欢这个过程,当我使用容器去创建自定义导航模式的时候我感到安全,因为我能够确定我在使用正确的工具。再补充一点,使用这个技术,你能够确保设备在旋转的时候立即更新层级结构中的视图。

就到这里,在Twitter上保持联系,如果有问题或者建议随后和我联系。
再见!

下载源代码

Follow @bitwaker

原文在此