多控制器-视图切换.
1.概述.
在iOS开发中,视图的切换是很频繁的,常用的视图切换有三种:
- UITabBarController
- 以平行的方式管理视图,各个视图之间关系不大;每个加入的视图都会进行初始化,不论当前显不显示在界面上.所以相对比较占内存.
- UINavigationController (PUSH)
- 以栈的方式管理视图,各个视图的切换就是压栈和出栈操作,出栈后的视图即被销毁.
- modal模态窗口.
- 以模态窗口的形式管理视图,当前视图关闭前其他视图上的内容无法操作。(遮盖)
- 自定义控制器切换.
2. UINavigationController
2.1 简介
导航控制器,用来组织有层次关系的视图,在UINavigationController中子控制器以栈(先进后出)的形式存储,只有在栈顶的控制器能够显示在界面中,一旦一个子控制器出栈则会被销毁;它必须有指定一个根控制器rootViewController才能创建,而且这个根控制器不会像其他子控制器一样被销毁,刚创建时,rootViewController即是栈底也是栈顶控制器;点击下一页就是控制器进栈,点击返回就是出栈(销毁).
导航条:
导航条的设置是根据栈顶控制器的navigationItem属性设置,导航条子控件是系统决定位置.高度44;
基本使用:
- 创建导航控制器:需要先创建其root控制器.
[[UINavigationController alloc] initWithRootViewController: rootController]
之后把导航控制器设为UIWindow的root控制器. - 添加子控制器,子控制器表现两种存储形式:viewControllers 和 childViewController数组.添加方式:
- nav.viewControllers = @[vc,vc2];
- [nav addChildViewController:vc];
- initWithRootViewController:vc;
- [nav pushViewController:vc animated:YES]; (默认封装了添加子控件方法)
- 跳转-进栈
[self.navigationController pushViewController:vc animated:YES];
- 主动出栈-手动跳转
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用于访问和设置导航条信息. (正在显示的导航条由栈顶即当前显示的子控制器来设置.)
self.navigationItem.title = @"haha"; (可以用self.title 代替 是系统内部封装的快速设置标题方法.)
//例:设置导航条左侧按钮
//方式一: 新建一个 UIButton *button = [[UIBarButtonItem alloc] init];
//给button设置图片,title等属性.
//导航条上子控件的位置是由系统决定的, 但是尺寸是由我们自己决定的.可以设置bounds; 可以使用自适应尺寸方法省得计算.
[button sizeToFit];
//最后,根据这个button来自定义创建导航条按钮.
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
//方式二: 直接调用创建方法 -- //点击触发调用addFriends方法.
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:@"Icon.png"] style:UIBarButtonItemStyleDone target:self action:@selector(addFriends)];
}
//创建并 跳转下一个子视图
-(void)addFriends{
//通过push导航到另外一个子视图
QQViewController *qqViewController=[[QQViewController alloc]init];
[self.navigationController pushViewController:qqViewController animated:YES];
}
//iOS 7 之后, 默认会把导航条上的按钮的图片渲染成蓝色,可用取消自动渲染
// image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
下一级子控制器中,如果要返回.
a. 返回上一个(栈顶出栈)popViewControllerAnimated: ;
b. 返回根控制器(出栈至栈底)popToRootViewControllerAnimated: ;
c. 返回指定控制器(根据存储形式下标).popToViewController: .
2.3 小结:
- UINavigationController默认显示一个根控制器,所以根视图必须指定,通过根控制器导航来到下一级子视图;
- 子视图中可以通过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];
第二种: 把控制器包装成导航控制器.这是给控制器添加导航条的最快方法.
4.主流框架
导航控制器和UITabBarController结合.一般由UITabBarButton做父控件:
原因:
- 由于导航控制器的导航条由栈顶控制器决定,如果导航做父控制器,那么在UITabBarController的子控件间切换时,上方导航始终不变.而UITableBarController的bar由各子控制器决定自己的.
- UITabBarController的子控制器数目由于均分UITabBar位置,数目有限制不超过五个.
5.多控制器的数据传递
5.1 segue跳转原理.
- 如果segue的type是push: 取得sourceViewController所在的UINavigationViewController, 再调用push方法压入栈中完成跳转.
- 如果segue的type是modal:调用sourceViewController的 presentViewController方法,将destinationViewController展示出来.
界面跳转:
执行[self performSegueWithIdentifier:@”id” sender:nil];时
- 执行segue.
- 根据Identifier去storyboard找对应的线,找到后,创建segue对象;
- 设置segue的sourceViewController(来源控制器),此时的self;
- 创建destinationViewController(目标),并设置器属性;
- 最后跳转前,会执行prepareForSegue方法,做跳转前的准备工作(一般是参数传值).;
- 最后系统就会自动调用self perform方法,进行跳转;
- perform底层
- 拿到来源控制器的导航控制器
- 进行 push 操作, push 到目标控制器
- 由 segue 拿到目标控制器
1.顺传:
源控制器把数据->目标控制器设置的属性接收.代码示例:
在sourceViewController中;
// 判断账号密码
if ([self.accountField.text isEqualToString: @"hm"] && [self.pwdField.text isEqualToString: @"123"]) { // 账号密码都正确
// 跳转界面
[self performSegueWithIdentifier:@"login2Contact" sender:nil];
//跳转前会执行prepareForSegue方法 并传入sugue 和sender :可通过传入的segue获取来源和目标控制器; sender是之前performSegueWithIdentifier传入的sender;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// 获取目标控制器.
UIViewController *vc = segue.destinationViewController;
// 顺传: 上一个控制器(来源控制器)把数据传递给下一个控制器(目标控制器)
vc.navigationItem.title = self.accountField.text;
}//可以传递模型数据.在目标控制器中设置属性接收即可.
2.逆传:
原理是:来源控制器把自己->目标控制器,再在目标中设置来源控制器的属性. 但是:这种耦合性太高,所以引入代理模式解耦;
代理=>有限制的对象间关联关系,通过把两个关联对象用协议束缚起来,达到解耦目的.
- 来源控制器 做 目标控制器 的代理,我们在目标控制器中定义协议,声明方法(把自己和数据作为参数)一般只传数据就够了,传自己是苹果代理编程习惯. 来源控制器 遵守协议,实现方法.
- 通过segue在目标中获取来源控制器.调用此方法,传入数据.
- 注意:调用前先做个判断(是否代理实现了方法) respondsToSelect;
- 注意:如果一个控制器segue了多个控制器,那么用segue获取目标控制器需要判断一下- isKindOfClass:
- 注意点:控制器顺传通常不能重写模型数据的set方法给子控件赋值,应为传值是在跳转之前传得,而此时控制器的view还没有加载,也就意味着子控件还没创建. 所以把值传过去后,在viewdidload中设置子控件属性.
- 其他逆传值方式:
- block逆传值:捕获自动变量的匿名函数指针.
- 通知机制
- 通过存储
- 通过AppDelegate定义全局变量(或者使用UIApplication、定义一个单例类等)
iOS主要使用代理来逆传值,代码如下:
目标控制器中:
// .h中 定义协议
@protocol HMAddViewControllerDelegate <NSObject>
// 声明方法.
- (void)addViewControllerWith:(HMAddViewController *)addVc didClickButton:(HMContact *)contact; //contact是模型数据
// 新增delegate
@property (nonatomic, weak) id<HMAddViewControllerDelegate> delegate;
//.m中
// 通知代理: 联系人控制器
if ([self.delegate respondsToSelector:@selector(addViewControllerWith:didClickButton:)]) {
[self.delegate addViewControllerWith:self didClickButton:contact];
}
来源控制器中.
//1. 在prepareForSegue方法中获取到目标控制器 . 让自己成为其代理
// 获取目标控制器(添加控制器)
HMAddViewController *addVc = segue.destinationViewController;
// 传递联系人控制器: 给目标控制器的 contactVc 属性赋值
addVc.delegate = self;
//2. 实现代理方法
- (void)addViewControllerWith:(HMAddViewController *)addVc didClickButton:(HMContact *)contact {
// 保存联系人模型
[self.contacts addObject:contact];
}
自定义多控制器切换.
某些时候,系统的导航控制器和UITabBarController不能满足项目需求, 需要我们自定义跳转效果.
1. 实现原理.
在主控制器上创建新控制器,并让新控制器View覆盖主View即可;
2. 细节与步骤.
- 创建新控制器,并将所有新控制器 变为主控制器的子控制器.
[self addChildViewController:[[HMOneViewController alloc] init]];
[self addChildViewController:[[HMTwoViewController 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,建立父子关系切换.(相当于1 给view配控制器,方便管理)
自定义的转场动画.
- 自定义动画代码因添加在切换控制器的方法中.
- 执行过渡动画的view要经历移除和添加事件.
- 动画执行调用CATransition类, 核心动画是添加在View的图层上的.
- (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.执行动画
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;
}];
*/
}