多控制器切换.
1.概述.
在iOS开发中,视图的切换是很频繁的,常用的视图切换如下:
- UITabBarController的Tab切换
- 以平行的方式管理视图,各个视图之间关系不大;每个加入的视图都会进行初始化,不论当前显不显示在界面上.所以相对比较占内存.
- UINavigationController 的 push和pop.
- 以栈的方式管理视图,各个视图的切换就是压栈和出栈操作,出栈后的视图即被销毁.
- modal模态窗口.
- 以模态窗口的形式管理视图presentation出现和dismissal关闭,当前视图关闭前其他视图上的内容无法操作。(遮盖)
- UICollectionViewController的布局转场,仅限于UICollectionViewController与UINavigationController结合的转场方式.
- 自定义控制器切换.
2. UINavigationController
2.1 简介
导航控制器,用来组织有层次关系的视图,在UINavigationController中子控制器以栈(先进后出)的形式存储,只有在栈顶的控制器能够显示在界面中,一旦一个子控制器出栈则会被销毁;它必须有指定一个根控制器rootViewController才能创建,而且这个根控制器不会像其他子控制器一样被销毁,刚创建时,rootViewController即是栈底也是栈顶控制器;
导航条:
导航条的设置是根据栈顶控制器的navigationItem属性设置,导航条子控件是系统决定位置.高度44;
基本步骤:
//1. 创建导航控制器
UINavigationController nav = [[UINavigationController alloc] initWithRootViewController:rootViewController];
//2. 添加子控制器,子控制器表现两种存储形式:viewControllers 和 childViewController数组.添加方式如下:
// nav.viewControllers = @[vc,vc2];
//[nav addChildViewController:vc];
//initWithRootViewController:vc;
//3. 跳转-进栈
[nav pushViewController:vc animated:YES]; //此方法默认封装了添加子控件方法.所以可以省略第二步
//4. 跳转-出栈
a. 返回上一个(栈顶出栈)popViewControllerAnimated: ;
b. 返回根控制器(出栈至栈底)popToRootViewControllerAnimated: ;
c. 返回指定控制器(根据存储形式下标).popToViewController: .
2.2 使用范例:
AppDelegate.m中:
//创建窗口
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
//1. 创建导航控制器的根控制器
ViewController *vc = [[ViewController alloc] init];
vc.view.backgroundColor = [UIColor redColor];
//2. 创建导航控制器
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.view.backgroundColor = [UIColor blueColor];
//引申:这里可以设置全局导航条的风格和颜色
[[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]];
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
//3.把导航控制器设为窗口根控制器
self.window.rootViewController = nav;
//设为主窗口并显示
[self.window makeKeyAndVisible];
在子控制器ViewController中
//对于当前子视图来说其父控制器就是器导航控制器,可以由此获取.
//self.navigationController == self.parentViewController;
//在子视图中,可以通过navigationItem用于访问和设置导航条信息. (屏幕正在显示的导航条信息就是栈顶子控制器的title)
self.navigationItem.title = @"haha"; //可以用self.title 代替 是系统内部封装的快速设置标题方法.
//例:设置导航条左侧按钮
//方式一: 新建一个
UIButton *button = [[UIBarButtonItem alloc] init];
//给button设置图片,title等属性.
//导航条上子控件的位置是由系统决定的, 但是尺寸是由我们自己决定的.可以设置bounds;无特殊要求可以使用自适应方法.
[button sizeToFit];
//最后,根据这个button来自定义创建导航条按钮.
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
//方式二: 直接调用系统创建方法,根据style选择不同风格
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:@"Icon.png"] style:UIBarButtonItemStyleDone target:self action:@selector(addFriends)];
}
//创建并转场下一个控制器
-(void)addFriends{
UIViewController *vc=[[UIViewController alloc]init];
self.hidesBottomBarWhenPushed = YES; // 隐藏底部tabBar.
[self.navigationController pushViewController: vc animated:YES];
}
//iOS 7 之后, 默认会把导航条上的按钮的图片渲染成蓝色,可取消自动渲染
//image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
下一级子控制器中,如果要返回.调用下面方法即可
a. 返回上一个(栈顶出栈)popViewControllerAnimated: ;
b. 返回根控制器(出栈至栈底)popToRootViewControllerAnimated: ;
c. 返回指定控制器(根据存储形式下标).popToViewController: .
2.3 小结:
- UINavigationController默认显示一个根控制器视图,所以必须指定rootViewController;
- 子视图中可以通过navigationController来访问导航控制器,它的childViewControllers来获取当前栈中所有子视图.(出栈的已被销毁)
- 导航控制器导航条由栈顶控制器设置
- 默认情况下除了根控制器之外的其他子控制器左侧都会在导航栏左侧显示返回按钮,点击可以返回上一级视图,同时返回按钮的title默认是上一级的标题. 可以通过backBarButtonItem属性修改;
3. UITabBarController
3.1 简介
UITabBarController是苹果专门为了利用页签切换视图而设计的,包含一个UITabBar控件,用户通过点击tabBar进行视图切换.为了尽可能减少视图之间的耦合,所有UITabBarController的子视图的相关标题、图标等信息均由各自子视图自己控制,UITabBarController仅仅作为一个容器存在。
结构:
和导航控制器类似:它的自带view用来存放导航条UITabbar和子控制器view两部分;
不同点:
- 它的导航条UITabbar在下方,高度49;
- 不依赖RootViewController创建;
- UITabbar的子控件UITabBarButton样式跟栈顶控制器无关,只跟对应的子控制器有关.子控件数由子控制器数决定.位置是自动均分的,所以一般分四个,典型例子微信和QQ.
- 导航控制器出栈会销毁子控制器,UITabBarController不会.
3.2 一般步骤
- 新建初始化UITabbarController;并设为窗口的root控制器.
- 设置UITabBarButton样式:
- 由对应子控制器的UITabBarItem设置.
- 包含:title标题,image图标,selectedImage选中状态图标,badgeValue右上角内容通知个数;(iOS7之后系统自动渲染)
- 添加 子控制器方式.
- [tb addChildViewController:c1];
- tb.viewControllers=@[c1,c2,c3,c4];
- 跳转:点击UITabBarButton自动跳转.
3.3 小结
- UITabBarController会一次性初始化所有子控制器,所以可以将视图控制器的tabBarItem属性设置放到init方法中设置.
- 每个视图控制器都有一个tabBarController属性,通过它可以访问所在的父控件UITabBarController.
- 每个视图控制器都有一个tabBarItem属性,通过它控制视图在UITabBarController的tabBar中的显示信息。
4. 模态窗口.
4.1 简介
模态窗口只是视图控制器的显示的一种方式.不依赖与控制器容器;通常用于显示较独立的内容,在模态窗口显示的时,其他视图的内容无法进行操作.
4.2 一般使用
使用起来比较容易,一般的视图控制器只要调用- (void)presentViewController:(UIViewController *)viewController animated: (BOOL)flag completion:(void (^)(void))completion
方法, 那么参数中的viewController就会以模态窗口的形式展现; 而此视图控制器再调用---(void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^)(void))completion
就会关闭模态窗口,回到原视图.
注意:modal出谁,谁才可以使用dismiss;
一般为了操作方便,会手动给模态窗口设置导航条,两种方式:
第一种:手动创建
//创建一个导航栏
UINavigationBar *navigationBar=[[UINavigationBar alloc]initWithFrame:CGRectMake(0, 0, 320, 44+20)];
//navigationBar.tintColor=[UIColor whiteColor];
[self.view addSubview:navigationBar];
//创建导航控件内容
UINavigationItem *navigationItem=[[UINavigationItem alloc]initWithTitle:@"Web Chat"];
//左侧添加登录按钮
_loginButton=[[UIBarButtonItem alloc]initWithTitle:@"登录" style:UIBarButtonItemStyleDone target:self action:@selector(login)];
navigationItem.leftBarButtonItem=_loginButton;
//添加内容到导航栏
[navigationBar pushNavigationItem:navigationItem animated:NO];
第二种: 把控制器包装成导航控制器的root控制器.这是给控制器添加导航条的最快方法.
补充1:目前APP的主流框架
导航控制器和UITabBarController结合.一般由UITabBarButton做父控件:
原因:
- 由于导航控制器的导航条由栈顶控制器决定,如果导航做父控制器,那么在UITabBarController的子控件间切换时,上方导航始终不变.而UITableBarController的bar由各子控制器决定自己的.
- UITabBarController的子控制器数目由于均分UITabBar位置,数目有限制,一般不超过五个.
补充2:使用Storyboard 工作时的切换
上面所说的控制器切换的触发方式主要有三种,包括:代码里调用相关方法, UINavigationBar和UITabBar自带的item点击操作, 最后一个就是Storyboard的Segue方式.如果你使用Storyboard进行开发,就需要了解一下.
segue跳转原理.
如果segue的type是push的,那么系统是取得sourceViewController所在的UINavigationViewController, 再调用push方法压入栈中完成跳转.
如果segue的type是Modal的,那么系统会调用sourceViewController的presentViewController
方法,将destinationViewController展示出来.界面跳转步骤:
在 storyboard 里设置 segue有两种方式:Button to VC,这种在点击 Button 的时候触发转场;VC to VC,这种需要在代码中调用performSegueWithIdentifier:sender:
。
prepareForSegue:sender:
方法是在转场发生前修改转场参数的最后机会。这点对于 Modal 转场比较重要,因为在 storyboard 里 Modal 转场的 Segue 类型不支持选择 Custom 模式,使用 segue 方式触发时必须在prepareForSegue:sender:
里修改模式。控制器间值的传递:
//1 .来源控制器把数据->目标控制器.代码示例:
//sourceViewController中;
[self performSegueWithIdentifier:@"login2Contact" sender:nil];
//此方法跳转前会执行prepareForSegue方法
//并传入sugue 和sender :可通过传入的segue获取来源和目标控制器; 而sender是之前传入的nil;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// 获取目标控制器.
UIViewController *vc = segue.destinationViewController;
// 顺传: 上一个控制器(来源控制器)把数据传递给下一个控制器(目标控制器) 很简单,直接传递即可.
vc.navigationItem.title = self.accountField.text;
}//可以传递模型数据.在目标控制器中设置属性接收即可.
// 2. 逆传值:
原理是:来源控制器把自己传给目标控制器,再在我们目前所在的目标vc中设置来源vc的属性. **但是:这种方式耦合性太高,所以引入delegate解耦;**
//代理:有限制的对象间关联关系,通过把两个关联对象用协议束缚起来,达到解耦目的.
1. 来源VC 做 目标VC 的代理,我们在目标VC中定义协议,声明方法(把自己和数据作为参数)一般只传数据就够了,但是传自己是苹果delegate编程习惯. 来源VC来 遵守协议,实现方法.
2. 通过segue在目标VC中获取来源VC.调用此方法,传入数据.
3. **注意1:**调用前先做个判断(是否代理实现了方法,避免崩溃) respondsToSelect;
4. **注意2:**如果一个控制器segue了多个控制器,那么用segue获取目标控制器需要判断一下- isKindOfClass:
5. **注意3:**控制器顺传通常不能通过重写模型数据的set方法给子控件赋值,因为这个传值是在跳转之前传得,而此时控制器的view还没有加
载,也就意味着子控件还没创建. 所以把值传过去后,一般在viewdidload中设置子控件属性.
6. 其他逆传值方式:
* block逆传值:捕获自动变量的匿名函数指针.
* 通知机制
* 通过文件存储
* 通过AppDelegate定义全局变量(或者使用UIApplication、定义一个单例类等)
//代理来逆传值,代码如下:
//目标VC:
// .h中 定义协议
@protocol HMAddViewControllerDelegate
// 声明方法.
- (void)addViewControllerWith:(HMAddViewController *)addVc didClickButton:(HMContact *)contact; //contact是模型数据
// 新增delegate
@property (nonatomic, weak) id delegate;
//.m中
// 通知代理: 联系人控制器
if ([self.delegate respondsToSelector:@selector(addViewControllerWith:didClickButton:)]) {
[self.delegate addViewControllerWith:self didClickButton:contact];
}
来源VC中.
//1. 在prepareForSegue方法中获取到目标控制器 . 让自己成为其代理
// 获取目标控制器(添加控制器)
HMAddViewController *addVc = segue.destinationViewController;
// 传递联系人控制器: 给目标控制器的 contactVc 属性赋值
addVc.delegate = self;
//2. 实现代理方法
- (void)addViewControllerWith:(HMAddViewController *)addVc didClickButton:(HMContact *)contact {
// 保存联系人模型
[self.contacts addObject:contact];
}
5.自定义多控制器切换.
最后,很多时候,系统的导航控制器和UITabBarController并不能满足项目需求, 这时就需要我们自定义跳转效果.
1. 实现原理.
其实很简单:只需在主控制器上创建新子控制器,并让这个子控制器的View覆盖主View即可;
2. 细节与步骤.
- 创建新控制器,并将所有新控制器 变为主控制器的子控制器.
[self addChildViewController:[[OneViewController alloc] init]];
[self addChildViewController:[[TwoViewController alloc] init]];
- 在主控制器中使用属性记录正在显示新控制器,方便使用.(由于前面都已经add,所以这里可以使用weak)
@property (nonatomic, weak) UIViewController *showingChildVc;
- 提供切换控制器方法, 为了减少代码冗余, 可以使用下标来访问 childViewcontrollers数组.
-(void)switchVC:(int)index { //即将要显示的子控制器索引
//1. 移除当前正在显示的其他子控制器的view
[self.showingChildVc.view removeFromSuperview];
//2. 添加index位置对应新控制器的view,并设置frame,
UIViewController *newVc = self.childViewcontrollers[index];
newVc.view.frame = CGRectMake(0, 44, self.view.frame.size.width, self.view.frame.size.height - 44);
[self.view addSuperview:newVc.view];
//3. 重新记录要显示的子控制器
self.showingChildVc = newVc;
}
注意: 一定要建立需切换控制器的父子关系, 否则某些系统事件 子控制器将无法接收并响应. 也无法获取父控制器的tabbar或导航控制器发送跳转消息.
自定义界面切换的方式和选择:
- 业务逻辑简单: 一个控制器多个View 切换即可
- 业务逻辑复杂时 使用多个控制器多个view,建立父子关系切换.(相当于一个view配一个控制器,方便管理)
补充:使用系统类型的转场动画.
- 自定义动画代码因添加在切换控制器的方法中.
- 执行过渡动画的view要经历移除和添加事件.
- 动画执行调用CATransition类核心动画, 这个核心动画是添加在View的图层layer上的.示例代码:
- (void)switchVc:(int)index
{
UIViewController *newVc = self.childViewControllers[index];
// 如果index对应的子控制器正在显示,就直接返回
if (newVc == self.showingChildVc) return;
// 0.保存新旧控制器的索引
NSUInteger newIndex = index;
NSUInteger oldIndex = [self.childViewControllers indexOfObject:self.showingChildVc];
// 1.移除其它控制器的view
[self.showingChildVc.view removeFromSuperview];
// 2.添加index位置对应控制器的view
newVc.view.frame = self.contentView.bounds;
[self.contentView addSubview:newVc.view];
self.showingChildVc = newVc;
// 3.执行动画,可根据subtype类型选择不同动画方式.
if (oldIndex == NSNotFound) return;
CATransition *animation = [CATransition animation];
animation.type = @"cube";
if (newIndex > oldIndex) {
animation.subtype = kCATransitionFromRight;
} else {
animation.subtype = kCATransitionFromLeft;
}
animation.duration = 1.0;
[self.contentView.layer addAnimation:animation forKey:nil];
//3. 也可使用UIView封装的动画,自己设计 .
// 动画
/*
[UIView animateWithDuration:0.5 animations:^{
CGRect oldFrame = self.showingChildVc.view.frame;
oldFrame.origin.x = - self.view.frame.size.width;
self.showingChildVc.view.frame = oldFrame;
newVc.view.frame = self.contentView.bounds;
}completion:^(BOOL finished) {
[self.showingChildVc.view removeFromSuperview];
self.showingChildVc = newVc;
}];
*/
}
补充: 完全自定义转场动画