Containing ViewControllers

Containing ViewControllers

For a project that I am currently working on I needed to implement a custom container view controller. I was feeling my way forward in the dark for the most part because this does not seem to be a widely used technique. Developers – understandably – favor reusing and skinning existing view controllers over creating new containers.

However there are some scenarios where you should prefer to make your own container because it greatly simplifies your code over trying to bend a UINavigationController or UITabBarController to your will. Do you remember the times when those two where the only two containers available?

I distinctly remember using a UINavigationController with hidden nav bar as a view controller to contain multiple full views. And you probably had do do your own view juggling and animations because for the most part the standard transitions would be useless. Fortunately we can no file this as a fond memory of the past and move on to implementing our own containers.

 

The first thing to wrap your head around is the notion that besides a hierarchy of views you now also have to have a consistent hierarchy of view controllers. Before iOS 5 people would often create a new view controller and then slap this VC’s view into an existing view hierarchy. Now more!

Nowadays you never would resort to this. Instead you use the mechanisms afforded by UIViewController to add and remove child view controllers.

Another notion is that we’ve trained ourselves to think of view controllers as being responsible for one entire full screen, think of the sub view controllers of a tab bar controller. But ever since UISplitViewController which arrived with the iPad this is no longer the true mantra. Viewcontrollers are supposed to manage a coherent region on the screen, that can be the entire screen for lack of space on an iPhone, but it can also be a content bar at one of the sides of the screen. Like UISplitViewController which has two sub-viewcontrollers, one for the left (“master”) panel and one for the right (“detail”) panel.

UIViewController provides two methods to add a view controller as a child and to remove it later. Those are part of the “UIContainerViewControllerProtectedMethods” category extension for UIViewController:

@interface UIViewController (UIContainerViewControllerProtectedMethods)
 
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeFromParentViewController;
 
@end

These two methods do exactly what their names suggest. Though what’s not quite obvious is how you are supposed to use them. And if and how you should combine these with addSubview and removeFromSuperview. Hence this exploration. Note: All this assumes we use ARC.

According to the docs it is up to us to define the kinds of relationships we want to model. Be it only a single VC visible at the time like nav controllers, or multiple that can be reached through tabs. Or even multiple VCs that are sort of like pages.

There are three possible things that you want to be able to do with your sub-viewcontrollers that have slightly different semantics:

  • Add it to your container
  • Remove it from your container
  • Transition to another view controller (i.e. add the new and remove the old)

For any of these you also want to be be assured that the 4 view delegate methods get properly called, as well as the new 2 delegate methods that fire before and after a VC moved to a new parent. Note that a parent of nil means that it was removed.

Why is this attention to the delegate messaging necessary? You probably use the view(Did|Will)(A|Disa)pear methods to do some last setup or teardown and so you are interested that they get properly called. And also there is an ugly warning in the console about unbalanced messages if you get something wrong here.

We’ll dive into greater detail with a sample. Let’s say we want to get an effect similar to a tabbed view controller. i.e. we have an array of view controllers and we want to switch between these. Since this container VC will be our app’s root view controller we want to show the first sub-VC when it shows.

Basic Setup

Let’s put the necessary bare bones IVARs in the implementation because we only need to have access to these inside our own ContainerViewController.

@implementation ContainerViewController
{
	NSArray *_subViewControllers;
	UIViewController *_selectedViewController;
	UIView *_containerView;
}

The subVCs will be a static array of view controllers that the developer can set. The selected VC will hold a reference to the currently showing VC and the container view will be the area where we want our sub VC’s view to be positioned. Let’s start by setting up the container view in the container’s loadView:

- (void)loadView
{
	// set up the base view
	CGRect frame = [[UIScreen mainScreen] applicationFrame];
	UIView *view = [[UIView alloc] initWithFrame:frame];
	view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
	view.backgroundColor = [UIColor blueColor];
 
	// set up content view a bit inset
	frame = CGRectInset(view.bounds, 0, 100);
	_containerView = [[UIView alloc] initWithFrame:frame];
	_containerView.backgroundColor = [UIColor redColor];
	_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
	[view addSubview:_containerView];
 
	// from here on the container is automatically adjusting to the orientation
	self.view = view;
}

I’ve colored the main view blue and the container view red for clarity. The sub-VCs will go in the red area and be automatically resized if we rotate the device.

The integration in the app delegate is a mere formality, add the import for the header, allocate an instance and set it as root VC.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 
	ContainerViewController *container = [[ContainerViewController alloc] init];	
 	self.window.rootViewController = container;
 
    [self.window makeKeyAndVisible];
    return YES;
}

Next we need a couple of view controllers to play the role of the sub-VCs. Let’s create a simple UIViewController subclass that has a UILabel as it’s main view, so that we can display something there to tell them apart. Do avoid overcomplicating things for this example we simple take the view’s own description in there. This way we see if the display changes.

- (void)loadView
{
	// set up the base view
	CGRect frame = [[UIScreen mainScreen] applicationFrame];
	UILabel *label = [[UILabel alloc] initWithFrame:frame];
	label.numberOfLines = 0; // multiline
	label.textAlignment = UITextAlignmentCenter;
 
	// let's just have this view description
	label.text = [self description];
	self.view = label;
}

I bet you never before had a view controller which consisted only of a UILabel. For Science! ?

Adding

Next we need to put a couple of these PageViewControllers into an array and have a method that allows us to set this array into our container. Let’s assume that you know how to create a property for the subViewControllers. In the app delegate we have these additional lines:

// make an array of 5 PageVCs
NSMutableArray *tmpArray = [NSMutableArray array];
 
for (int i=0; i<5; i++)
{
	PageViewController *page = [[PageViewController alloc] init];
	[tmpArray addObject:page];
}
 
// set these as sub VCs
[container setSubViewControllers:tmpArray];

So much for basic setup. Now let’s override the setter for the sub VCs to select the VC at index 0 and present it. Though we cannot present it in the setter, because the view might not have been loaded yet and so our _containerView IVAR is still nil.

- (void)setSubViewControllers:(NSArray *)subViewControllers
{
	_subViewControllers = [subViewControllers copy];
 
	if (_selectedViewController)
	{
		// TODO: remove previous VC
	}
 
	_selectedViewController = [subViewControllers objectAtIndex:0];
 
	// cannot add here because the view might not have been loaded yet
}
 
@synthesize subViewControllers = _subViewControllers;

Instead we do it at the latest possible moment, that would be in the viewWillAppear because here we are guaranteed that the loadView has taken place already. A nice lazy thing, if we find that the selected VC already has self as the parent then there’s nothing to do.

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];
 
	if (_selectedViewController.parentViewController == self)
	{
		// nowthing to do
		return;
	}
 
	// adjust the frame to fit in the container view
	_selectedViewController.view.frame = _containerView.bounds;
 
	// make sure that it resizes on rotation automatically
	_selectedViewController.view.autoresizingMask = _containerView.autoresizingMask;
 
	// add as child VC
	[self addChildViewController:_selectedViewController];
 
	// add it to container view, calls willMoveToParentViewController for us
	[_containerView addSubview:_selectedViewController.view];
 
	// notify it that move is done
	[_selectedViewController didMoveToParentViewController:self];
}

The above shown sequence takes care of calling the viewWillAppear, viewDidAppear, willMoveToParentViewController and didMoveToParentViewController. Notice that all but the last are done for you automatically, but for some strange reason the didMove is not. So we have to do that manually. Upon starting our Demo we now see the VC at index 0.

Next we add the capability to transition from one VC to the next.

Transitioning

To move between the child VCs we’ll add a swipe gesture recognizer to our container. If we swipe left we want to go to the VC with a lower index in the array. If we swipe right we want to go higher. No wrapping. In loadView we add:

// add gesture support
UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[view addGestureRecognizer:swipeLeft];
 
UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)];
swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
[view addGestureRecognizer:swipeRight];

And the implementation of swipe is as follows. For simplicity’s sake we use two separate gesture recognizers because Apple does not provide an easy way to determine the swipe direction if you combine to directions.

- (void)swipeLeft:(UISwipeGestureRecognizer *)gesture
{
	if (gesture.state == UIGestureRecognizerStateRecognized)
	{
		NSInteger index = [_subViewControllers indexOfObject:_selectedViewController];
		index = MIN(index+1, [_subViewControllers count]-1);
 
		UIViewController *newSubViewController = [_subViewControllers objectAtIndex:index];
 
		[self transitionFromViewController:_selectedViewController toViewController:newSubViewController]; 
	}
}
 
- (void)swipeRight:(UISwipeGestureRecognizer *)gesture
{
	if (gesture.state == UIGestureRecognizerStateRecognized)
	{
		NSInteger index = [_subViewControllers indexOfObject:_selectedViewController];
		index = MAX(index-1, 0);
 
		UIViewController *newSubViewController = [_subViewControllers objectAtIndex:index];
 
		[self transitionFromViewController:_selectedViewController toViewController:newSubViewController]; 
	}
}

The logic to transition from one VC to another we package in transitionFromViewController:toViewController:. This is the really interesting part. There is a convenient method that again takes care of most of the boring work of adding and removing views. And again some non-obvious additional messaging is necessary to get it perfect.

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController
{
	if (fromViewController == toViewController)
	{
		// cannot transition to same
		return;
	}
 
	// animation setup
	toViewController.view.frame = _containerView.bounds;
	toViewController.view.autoresizingMask = _containerView.autoresizingMask;
 
	// notify
	[fromViewController willMoveToParentViewController:nil];
	[self addChildViewController:toViewController];
 
	// transition
	[self transitionFromViewController:fromViewController
					  toViewController:toViewController
							  duration:1.0
							   options:UIViewAnimationOptionTransitionCurlDown
							animations:^{
							}
							completion:^(BOOL finished) {
								[toViewController didMoveToParentViewController:self];
								[fromViewController removeFromParentViewController];
							}];
}

And with that we have our transitions in the can.

You have a number of UIViewAnimationOptionTransition-s available, but you don’t have to settle for these. You can also pass 0 for the options and instead provide all animations you like the two views to execute in the animations block.

Before I discovered is method I was using the previous method of animating the views. Though this has a side-effect that we might not like in this case. Typically you want the “will” delegate methods to fire before the transition and the “did” to follow afterwards. If you animate the views yourself then iOS 5 will take care of sending these messages for you but it does so together. This looses us the ability to differentiate between stuff we want to do before and after the appearing and disappearing of the view controller.

Conclusion

It took me quite a bit of experimenting to also get all the messaging happening and equally balanced. The above sample has that working as it should.

But once you do figure out the two techniques outlined in this article you are on the road to implement your own view controller containment like you never did anything else.

One thing that I wasn’t able to figure out so far is why the transition method always adds the new view controller’s view to the container view controller’s main view. This simplifes the process somewhat because you don’t have to know at which stage in the transition it is ok to add and remove the views. But at the same time you might have a situation where you don’t want the animation to occur over the entire area of the container view controller.

For this scenario I can only think of covering up the parts with extra sub views. Or we could stack multiple container view controllers and have one to only cover the region where where have the container view. This could then clip its subviews and thus be only care for this area.

The main advantage of any kind of view controller containment is that rotation messages (should|will|did) reach the lowest leaves of your view controller tree. Unless you disable that by means of overriding automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers and returning NO.

But who would really want that when we have waited for so long for public API to have these being forwarded? Working with view controller containers I get a feeling that they greatly simplified creating complex user interfaces consisting of multiple parts.

The project for this tutorial is in my Examples GitHub repository.

转载于:https://www.cnblogs.com/stephen-init/p/5082560.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.本版本的需要2.0框架支持<br>2.功能:<br> 支持数据缓存<br> 支持分页方式多样化<br> 支持SQL数据库<br> 支持GridView数据表格<br><br> 请注意若您需要用到图片分页模式请拷贝Images文件夹.如果需要定义自己的图片样式,请不要改变图片的名称即可.<br><br>3.不足 <br> 控件暂时只支持SQL数据库其他数据库的支持接口以完成还没来得及写,有时间我会升级<br> 控件暂时不支持存储过程<br> 现在发布的也只是个BATE版有BUG欢迎指正<br> 邮箱:wensifww@163.com<br> QQ:24754991<br> QQ群:24604453<br><br> <br>4版本升级<br> (1) SqlPage 1.0.1.0 版本:(修改时间:2006-11-23)<br> 1.修正了在不使用缓存的情况下控检不显示数据的BUG<br> 2.去除HasGridView属性该为控件自行判断数据表格类型<br> 3.增加在用缓存模式下改变SQL语句获得新数据功能<br> 4.在绑定表格后对表格操作后绑定更加简单只需添加SqlPage1.DataBind()方法即可<br> 去除程序员写繁琐的绑定CODE<br> 5.添加了ControlToPaginate内置属性用于获得当前控件所绑定的数据表格控件<br> <br> (2)SqlPage 1.0.1.2 版本:(修改时间:2007-1-27)<br> 1.修正了表格控检在多层中SqlPage找不到绑定控件的BUG。 <br> 2.应很多朋友呼吁删除排序字段开发排序SQL语句,由程序员自己写<br><br> (3)SqlPage 1.1.4.8 版本:(修改时间:2007-7-31)<br> 1.优化数据读取(支持百万级数据读取)<br> 2.分页控件资源集成化,不需要用户Copy控件资源文件<br> 3.分页控件排序优化:默认情况下为表主键排序<br> 4.修正分页控件在删除当前索引也中所有行不能自动转到上一页的错误<br> 5.添加分页的排序方式<br> 6.自定义排序的字段<br> 7.修正在除GridView外表格控件绑定空数据库时出错问题<br> 8.修正用户SQL语句后面带分号出错<br> 9.修正降序排序单页显示成升序的错误<br> (4)SqlPage 1.2.2.3 版本:(修改时间:2007-8-9)<br> 1.修正MSSQL用户SQL语句中同时带有TOP 和ORDER BY 语句程序不能识别问题<br> 2.添加了以分页段的分页样式丰富了分页样式<br> 3.做了一个控制分页控件样式表(CSS)的DOME(有朋友不知道怎么去控制这里简单的 说明使用方法,很简单)<br> 4.修正MSSQL用户SQL语句中ORDER BY再次绑定的问题 <br> 5.修正控件分页模式在图片分页模式下控件编辑状态出错问题<br> 6.新添了控件自动添加前缀功能(Wensi)<br> 7.新增分页跳转类型,两种:下拉列表框和文本框,系统默认为下拉列表框,(在实际开发中我们发现下拉列表框在分页数达到1000级以上的时候明显变慢,所以建议大家在分页数很大时使用文本框类型这样可以明显提高效率)<br> 8.新增控件样式CSS文件是否用系统自带的样式文件还是用户自定义样式文件属性 HasSystemPageStyle<br> (5)SqlPage 1.2.5.3 版本:(修改时间:2007-10-24)<br> 1.修改了在2次绑定无数据时现实前一次绑定数据的BUG<br> 2.修改了在页面索引为-1时系统不能正常恢复显示数据BUG<br>如果您在使用中发现BUG或有很好的建议请联系我,让大家一起参与进来维护好这个控件.<br>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值