爬爬爬之路:UI(八)UINavigationController 界面传值

UINavigationController

工作原理

导航视图控制器, 是iOS应用中最常用的多试图控制器之一, 它用来管理多个视图控制器.

具体来说, 导航视图控制器是一个用来管理一组有层级关系的视图控制器的控制器

UINavigationController自带一个半透明的导航条(UINavigationBar).
导航条竖屏状态下的高度是44. 横屏状态下的高度是32
状态栏的高度是20
在整个半透明区域的高度是64.
不要误会, 其实状态栏的颜色是默认被导航条的颜色渗透的, 在不进行具体定制的时候, 状态栏和导航条的颜色一致. 但实际上导航条是不包括状态栏的

UINavigationController是通过管理栈的模式来管理添加在它之上的一组视图控制器的.
栈底放的是根视图控制器. 根视图控制器是不可出栈的, 只会随着UINavigationController的消失而消失.

UINavigationController常用的初始化方法是:

// 在创建UINavigationController之初就给定它一个根视图控制器(也就是应用一开始显示的那个界面对应的视图控制器, 注意安装动画不属于应用运行的过程)
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
// 当一个视图控制器被添加到栈中时, 改视图控制器中的navigationController属性会自动赋值为本UINavigationController

当需要切换界面的时候, 就把下一个界面压入栈中. 当切换会上一个界面时, 就把本界面出栈. UINavigationController自动让处于栈顶的界面显示给用户.

关于UINavigationController, UIViewController中有两个很重要的属性
1. @property(nonatomic,readonly,retain) UINavigationItem *navigationItem;
获得本视图上的导航条上的按键区. UINavigationItem管理三个区域, 从左往右的顺序依次是左侧的按键区(比如返回上一个界面键) 中心区(比如标题, 或者标题位置的SegmentControl) 右侧按键区(各种功能键, 比如搜索, 分享链接等等), 之所以这个属性不是属于UINavigationController的属性, 而是UIViewController的属性, 是因为每个界面在导航栏上的功能按键是不一样的. 最简单的例子是每个界面的title. 每个界面的标题都不一样, 还有每个界面的导航栏功能也不一样, 而导航栏是共有的, 所以只能将导航栏的具体定制属性放在UIViewController上. 显示按键的前提UIViewController所属的导航试图控制器不为空.UINavigationItem属于M层.
2. @property(nonatomic,readonly,retain) UINavigationController *navigationController;
获得所属的导航试图控制器

常用属性

  1. @property(nonatomic,readonly,retain) UIViewController *topViewController;
    获取当前处于栈顶的视图控制器
  2. @property(nonatomic,readonly,retain) UIViewController *visibleViewController
    获得当前显示的视图控制器
  3. @property(nonatomic,copy) NSArray *viewControllers;
    获得目前处于栈中的所有视图控制器
  4. @property(nonatomic,readonly) UINavigationBar *navigationBar
    获得自带的导航条

导航视图控制器除了放在上方的导航条, 还有一个在屏幕下方的工具条(ToolBar). 它默认是被隐藏的, 本人觉得它太丑了 这才是它被隐藏的根本原因. 所以这里暂时不讨论.

定制导航条(UINavigationBar)

导航条在iOS7之后默认是半透明的. 当导航条是半透明的情况下(或者单纯设置背景颜色时), 铺设当前界面的坐标是要从整个屏幕的左上角(0,0)开始计算的

当导航条半透明属性关闭或者设置了导航条的背景图片时, 铺设当前界面的坐标是从导航条下边(0,64)开始计算的

关闭导航条半透明状态

@property(nonatomic,assign,getter=isTranslucent) BOOL translucent;

// 调用方法
self.navigationBar.translucent = NO;

值得注意的是, 当此句允许后, 每个界面的导航条都会变成非半透明状态.
若此句在第二个界面中运行时, 要注意界面跳回到第一个界面时, 第一个界面的原点坐标会从(0, 0)变换到(0, 64), 需要对第一个界面控件的坐标要进行重新.

具体属性见图
关系图

barTintColor 设置导航条的颜色
setBackgroundImage:forBarMetrics: 导航条加背景图片
第二个参数一般选择 UIBarMetricsDefault 就好
对于第一个参数, 传入的是一个UIImage
不过当UIImage的高度不同的时候, 显示的效果是截然不同的

  1. 图片高度小于44, 效果是平铺
  2. 图片高度等于44, 效果是正好只铺满导航栏, 并且把状态栏漏出来
  3. 图片高度大于44小于64, 效果也是平铺
  4. 图片高度等于64, 效果是正好覆盖导航条和状态栏

UINavigationItem

UINavigationItem属于MVC中的M。封装了要显⽰在UINavigationBar上
的数据。
1. title
标题. 设置NavigationBar中间区域的文本信息
2. titleView
标题视图. 设置NavigationBar中间区域的控件信息
3. leftBarButtonItem
左按钮 设置NavigationBar左侧的按键.
4. rightBarButtonItem
右按钮 设置NavigationBar右侧的按键

注意, 左侧和右侧按钮不是UIButton. 而是UIBarButtonItem
两者没有继承关系.
初始化UIBarButtonItem有三种方法

- (instancetype)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action;
/* 
定制Item的样式, 这里的样式是系统预设好的样式.
UIBarButtonSystemItem是一个结构体
常用的有
UIBarButtonSystemItemDone,
UIBarButtonSystemItemCancel,
UIBarButtonSystemItemSearch
等等
*/
- (instancetype)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action;
/*
定制显示为文字的按键
*/
- (instancetype)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action;
/*
定义一个图片自定义的按键.
style常用的是
UIBarButtonItemStylePlain
*/

这三个初始化方法, 只是初始化出来了一个可以添加到NavigationBar上的按键
至于是添加在左侧, 还是右侧需要程序员自己定义
左侧和右侧可以只添加一个按键, 也可以添加一组按键

// 添加一个按键 rightItem 是一个UIBarButtonItem的对象
self.navigationItem.rightBarButtonItem = rightItem;

// 添加一组按键 rightItems是一个数组, 数组里存放了多个UIBarButtonItem对象
self.navigationItem.rightBarButtonItems = rightItems;


// 设置标题
self.navigationItem.title = @"首页";
// 设置标题位置的控件
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100, 150, 100, 44)];
view.backgroundColor = [UIColor redColor];
self.navigationItem.titleView = view;
// title和titleView是不兼容的. 如果设置了其中一个, 就不要设置另一个. 根据需求选择设置的是哪一个.



/*
再次声明, 这是定制一个页面的导航条标题和左右按键.  每个界面都需要对自己的导航条进行定制.
self.navigationItem 中的self是一个ViewController的对象
本语句是给当前的ViewController的导航条进行定制.
*/

navigationBar是给所有视图共有的导航条进行定制
navigationItem是给具体的某一个视图的导航条进行按键和标题的定制


界面传值

界面传值指的是被同一个UINavigationController管理的视图控制器之间的传值
分为从前往后传值和从后往前传值两种传值方式.

在说界面传值之前, 先说怎么进行页面的跳转和怎么让UINavigationController管理某一个视图控制器.

上文提过, UINavigationController的初始化方法为

- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;

// 此时已经将根视图控制器填入栈中, 也就是让UINavigationController管理
// 此时导航视图控制器管理的视图只有根视图控制器


// 跳转到新界面. 如SecondViewController
SecondViewController *second = [[SecondViewController alloc] init];
[self.navigationController pushViewController:second animated:YES];
[second release];
// 调转页面用的push方法. 此时就SecondViewController压入栈中, 同时就将SecondViewController

// 跳回第一个界面
// 这里的self是此时需要会跳界面的ViewController对象 下同.
[self.navigationController popViewControllerAnimated:YES];

// 跳回到之前的(栈中的)某一个界面
// 这里的aViewController可以通过self.navigationController.viewControllers[x];进行获取
[self.navigationController popToViewController:aViewController];

// 直接跳回到根视图
[self.navigationController popToRootViewControllerAnimated:YES];

可以注意到, 跳转界面(push)的时候, 可以先新建一个对象, 再跳转. 所以在跳转的时候, 可以获得这个对象.

所以, 当从前往后传值的时候. 可以直接给后一个界面的属性进行赋值(该属性不能是控件类的属性). 然后在后一个界面的viewDidLoad方法中直接对该属性进行操作.

一 从前往后传值

例如: 需要将第一个界面(RootViewController)中管理的一个UITextField中的值传给第二个界面(SecondViewController)中的UITextField.

// 首先先在第二个界面中的.h文件中声明一个可以接受UITextField.text信息的属性
// 如
@property (nonatomic, retain) NSString *str;

// 让后在RootViewController.m文件中的跳转方法里写
SecondViewController *second = [[SecondViewController alloc] init];
second.str = self.textField.text; // 给第二个界面的str属性赋值
[self.navigationController pushViewController:second animated:YES];
[second release];


// 最后在SecondViewController中的viewDidLoad方法中
self.textField.text = self.str; // 直接给需要传值的控件赋值

需要注意的一点是, 当第一个界面给第二个界面用属性传值的时候, 第二个界面中用来接受值的属性不建议是控件类的属性.

因为控件类对象是在loadView 或者viewDidLoad方法中进行铺设的, 而事实上这两个方法是在第一个界面从第二个界面的跳转过程中触发的.

比如以下代码

- (void)clickButton:(UIButton *)button {
    NSString *str = @"123";
    SecondViewController *secondVC = [[SecondViewController alloc] init];
    secondVC.textField.text = str;
    [self.navigationController pushViewController:secondVC animated:YES];
    [secondVC release];
}

在button响应方法中实现跳转. 问题来了, 此时界面是在哪一行跳转的, 第二个界面是在何时加载的.

这里不卖关子, 直接解释

事实上跳转是发生在这个方法结束. 当整个方法的代码运行完毕, 才开始界面的跳转.
而界面加载是发生在跳转过程中的. 也就是说第二个界面中的viewDidLoad方法是在界面跳转中运行的.
也意味当执行到上述第三代码后, SecondViewController虽然开辟控件也初始化了, 但事实上这时候只是给SecondViewController的对象开辟了一个空间. 但是它的属性对象self.view 还未被 加载到 (注意加载和初始化是两个动作)SecondViewController对象上. 换一句话说此时第四行代码secondVC.textField 提到的属性textField 还没初始化出来, 它的初始化时间至少是在self.view开始加载以后. 所以此时这行代码中的textField是无法被赋值的.

我们知道当一个对象被创建出来, 它的属性指针也会被一同创建. 只要给属性赋值, 对象就可以获得它对应属性的值. 比如创建一个对象, 让对象的指针指向这个指针即可让属性有值.

从上述原理看来, 其实把代码改写成

- (void)clickButton:(UIButton *)button {
    NSString *str = @"123";
    SecondViewController *secondVC = [[SecondViewController alloc] init];
    UITextField *aTextField = [[UITextField alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    aTextField.text = str;
    secondVC.textField.text = aTextField;
    [self.navigationController pushViewController:secondVC animated:YES];
    [secondVC release];
}


// 然后在SecondViewController的viewDidLoad方法中

[self.view addSubview:self.textField];

这样完全行得通, 但是这样就破坏了MVC模式, 这会导致一个界面有两个Controller管理, 大大增加了两个Controller类之间的耦合性

所以还是建议先用一个非控件类的属性来接受前一个界面传过来的值, 再在本类的.m中直接赋值给需要该值的控件.

二 从后往前传值

这里主要介绍两种方法:
1. delegate传值
2. block传值

以下默认都是RootViewController作为第一个界面, SecondViewController作为第二个界面
第一个界面的控件为一个用来接受第二个界面值的UILabel, 和一个响应跳转方法UIButton

第二个界面的控件为一个用于输入值的UITextField, 和一个响应跳回方法的UIButton

为了赋值取值方便, 将UILabel和UITextField都设置成各自的属性

delegate传值

第一步 先在第二个界面定义一个协议

@protocol SecondViewDelegate <NSObject>

- (void)changeLabel:(NSString *)str;

@end

第二步 在第二个界面的.h中声明一个遵守协议的代理

@property (nonatomic, assign) id<SecondViewDelegate> delegate;

第三步 在第二个界面的button响应方法中:

- (void)clickButton:(UIButton *)button {
    // 安全判断, 若代理实现了协议方法, 就调用协议方法
    if([_delegate respondsToSelector:@selector(changeLabel:)]) {
        [_delegate changeLabel:self.textField.text];
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

第四步 在第一个界面的button响应方法中添加一句设置第二界面的代理为自己

// 在此之前别忘了要先遵守协议

- (void)clickButton:(UIButton *)button {
    SecondViewController *secondVC = [[SecondViewController alloc] init];
    secondVC.delegate = self;
    [self presentViewController:secondVC animated:YES completion:nil];
    [secondVC release];
}

第五步 实现协议方法

- (void)changeLabel:(NSString *)str {
    self.label.text = str;
}

Block传值

第一步 先在第二个界面中声明block

typedef void (^myBlock)(NSString *str);

@interface SecondViewController : UIViewController
@property (nonatomic, copy) myBlock block;

@end

第二步 在第二个界面中的button响应方法 调用block

// SecondViewController.m
- (void)clickButton:(UIButton *)button {
    self.block(self.textField.text);    // 调用block方法
    [self dismissViewControllerAnimated:YES completion:nil];
}

第三步 在第一个界面中的button相应方法 实现block

// RootViewController.m
- (void)clickButton:(UIButton *)button {
    SecondViewController *secondVC = [[SecondViewController alloc] init];
    secondVC.block = ^void (NSString *str) {
        self.label.text = str;
    };
    [self presentViewController:secondVC animated:YES completion:nil];
    [secondVC release];
}

两个方法从步骤而言, block比较简单, 但是从代码逻辑的清晰度而言, delegate会显得更容易理解一点.

其实两者用到的原理都一样, 都是用到了将方法的声明和实现分离的手法
两者不太一样的地方是 delegate是方法的调用是在第一个界面中, 而block的是在第二个界面中调用, 这也正是block传值的步骤更少的原因.

不过从代码的逻辑性而言, 更推荐delegate方法.

关于delegate和block传值引发的一些思考和理解

简单归纳一下两者的共性
首先均是在需要传出值(消息)的地方(这里是第二个界面) 声明一个方法

然后在需要的地方(跳转方法)调用方法, 调用的同时可以传入实参

最后在需要接受值(消息)的地方(这里是第一个界面) 中完成这个方法的实现
利用相同类型和个数的形参获得实参, 再对形参进行数据转移(赋值)或者是其他操作.

这里将函数或者方法分为三个步骤: 声明, 实现, 调用 来说明.

不同于常规的方法和函数, 常规的方法或者函数 是先将声明和实现固定, 调用不固定
比如在类创建之初 就已经将其方法的声明和实现写好了. 至于在程序的主流程中调不调用这个方法就不一定了

而block和delegate这种模式 是将声明和调用固定, 实现不固定
也就是先将方法声明出来, 然后在需要发送消息(实参)的地方 直接调用.
具体的实现是在需要接受该消息(实参), 的时候完成, 实现灵活而自由. 相对于常规的方法和函数. 这种形式显得非常灵活, 但是代价是理解起来相对有难度.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值