iOS开发之UI篇(14)—— UINavigationController

版本

Xcode 10.2
iPhone 6s (iOS12.4)

目录

版本
继承关系
简介
创建
切换视图控制器
UINavigationBar & UIToolBar
其他方法属性
返回按键

继承关系

UINavigationController : UIViewController : UIResponder UIResponder : NSObject

简介

导航控制器是容器视图控制器,其管理导航界面中的一个或多个子视图控制器。在这种类型的界面中,一次只能看到一个子视图控制器。视图控制器间的切换: 使用动画在屏幕上推出新的视图控制器,从而隐藏先前的视图控制器。点击顶部导航栏中的后退按钮将删除顶视图控制器,从而显示下方的视图控制器。
以Apple自家的设置App为例, 点击Setting中的General会推出General界面, 点击Auto-Lock会推出Auto-Lock界面. 如下图:

导航控制器

UINavigationController使用有序数组(称为导航堆栈)管理其子视图控制器。数组中的第一个视图控制器是根视图控制器 (没有rootViewController属性, 使用viewControllers[0]获取),表示堆栈的底部。数组中的最后一个视图控制器是堆栈中最顶层视图控制器(topViewController)。我们可以使用segue或使用此类的方法从堆栈中添加和删除视图控制器, 还可以使用导航栏中的后退按钮或使用左边滑动手势来移除最顶层的视图控制器。

结构

结构

结构图中主要有三个部分: 顶部的UINavigationBar, 底部默认隐藏的UIToolBar, 以及中间content部分存放子视图控制器的view.
UINavigationController是一个容器视图控制器 , 也就是说,它将其他视图控制器的内容嵌入其自身内部。我们可以使用其view属性访问导航控制器的视图。
虽然导航栏和工具栏视图的内容发生更改,但视图本身是不变的。实际更改的唯一视图是导航堆栈上最顶层视图控制器提供的自定义内容视图。

管理的对象

管理的对象

如图, 导航控制器主要管理四个对象: 子视图控制器, 导航栏, 工具栏, 其delegate对象.

  1. 导航控制器管理子视图控制器的入栈出栈以及显示等, 中间部分显示的view就是顶层视图控制器topViewController的view。
  2. 导航栏始终存在并由导航控制器本身管理,导航控制器使用其子视图控制器提供的内容更新导航栏. 比如, 导航栏上的返回按钮后面紧跟上一个界面的title。
  3. 类似的, 当toolbarHidden属性为NO时,导航控制器使用其子视图控制器提供的内容更新工具栏。
  4. UINavigationController依赖其delegate对象来协调自身的行为. 例如, delegate对象(视图控制器)可以实现UINavigationControllerDelegate的代理方法, 从而自定义动画过渡, 或者重新制定导航控制器的指向.

创建

新建模板App. 新建类(比如NavigationController)继承自UINavigationController, storyboard中拖入一个UINavigationController, class改为我们自己创建的类(NavigationController). 拖入的UINavigationController默认的rootViewController为UITableViewController, 删除这个UITableViewController, 然后从UINavigationController引一条线(按Ctrl或者鼠标右键)到原来的ViewController, 选择root view controller, 此时ViewController即变成跟视图控制器. 最后一步, 将ViewController左边的小箭头”->”拖动到UINavigationController, 这个小箭头代表初始启动App调用的控制器, 我们也可以改变右边面板中的Is Initial View Controller选项来改变初始控制器.
完成以上操作, 工程即包含了NavigationController导航控制器和ViewController初始视图控制器.
使用代码创建思路类似, 此处不演示.

切换视图控制器

1. 代码切换
当不使用导航控制器的时候, 我们使用UIViewController的以下方法来跳转视图

// 载入视图控制器 (入栈)
- presentViewController:animated:completion:
// 移除视图控制器 (出栈)
- dismissViewControllerAnimated:completion:

而使用导航控制器时, 我们可以使用

// 载入视图控制器 (入栈)
- showViewController:sender:
// 载入视图控制器 (入栈)
- pushViewController:animated:
// 移除视图控制器 (出栈)
- popViewControllerAnimated:
// 移除到指定视图控制器 (出栈)
- popToViewController:animated:
// 移除到根视图控制器 (出栈)
- popToRootViewControllerAnimated:

关于"- showViewController:sender:"和"- pushViewController:animated:"的区别

  1. 当当前视图控制器是导航控制器的子控制器时 (即self.navigationController不等于nil), 调用”- showViewController:sender:”方法, 系统通过一番处理后最终会调用”- pushViewController:animated:”方法并默认使用动画效果 (animated=YES).
  2. 当不使用导航控制器时 (即self.navigationController等于nil), 调用”- showViewController:sender:”方法, 系统通过一番处理后最终会调用”- presentViewController:animated:completion:”方法并默认使用动画效果 (animated=YES).
  3. 当使用UISplitViewController分离视图控制器时, 最好调用”- showViewController:sender:”方法, 具体此处不讨论.

2. 使用segue
有两个view controller A 和 B, 在A中的某一子控件(一般是button)按住Ctrl或者鼠标右键拉一条线至B, 会出现以下选项.

segue类型

选择show, 然后segue就形成了. 使用的时候点击A中的这个触发子控件就可以跳转至B了.

关于segue的几种类型 (已经遗弃的类型不予讨论)

  1. Show
    对应代码方法-showViewController:sender:
    将目标视图控制器推到导航堆栈上,从右向左滑动,提供返回按钮. 如果未嵌入导航控制器,它将以模态方式显示.
    示例:点击某一内容显示另一个视图界面铺满屏幕
Show
  1. Show Detail
    对应代码方法-showDetailViewController:sender:
    用于拆分视图控制器(UISplitViewController)时,在展开的2列界面中替换详细/辅助视图控制器,否则如果折叠为1列,则将推入导航控制器.
    示例:在"设置"中点击"通用"选项, iPhone推出完整界面铺满屏幕, iPad推出第2列界面.
Show Detail
  1. Present Modally
    对应代码方法-presentViewController:animated:completion:
    呈现出的各种带动画效果的视图控制器,覆盖前一个视图控制器. 在iPhone中, 新的VC从底部动画向上弹出并覆盖整个屏幕; 在iPad上通常将新的VC显示为居中的框,使原来的VC变暗.
    示例:在“设置”中选择“触摸ID和密码”
Present Modally
  1. Present As Popover
    对应代码方法 iPad(-presentPopoverFromRect:inView:permittedArrowDirections:animated:), iPhone(-presentViewController:animated:completion:)
    在iPhone中,默认情况下新的VC会在整个屏幕上以模态方式显示; 在iPad上运行时,新的VC以弹窗形式显示在点击处的旁边,点击此弹出框外的任何位置都会回收推出这个新VC.
    示例:点击日历中的+按钮
Present As Popover
  1. Custom
    我们可以实现自己的自定义segue并控制其行为, 但是不推荐使用已经废弃的segue类型, 因为这些segue类型在iOS 8中已弃用:Push,Modal,Popover,Replace。

3. 使用代码+segue
在storyboard中, 我们经常用segue线把相关的VC连接起来, 使之看起来更整齐有序. But, 有时候我们不希望绑定segue的起点为某个指定的触发按键, 而是希望在恰当的时机使用代码来调用segue. 那么我们可以这样做: 从VC1界面的View Controller处引线至VC2界面, 点击生成segue线, 在右边面板中找到Identifier属性并自定义一个值segueVC2ID, 然后在VC1中跳转代码如下:

[self performSegueWithIdentifier:@"segueVC2ID" sender:nil];

如果我们绑定了segue的起点为某个触发按键, 但是有些情况下我们希望做拦截判断是否真的需要跳转, 可以在VC1中实现如下方法:

- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    
    if ([identifier isEqualToString:@"segueVC2ID"]) {
        return NO;
    }
    
    return YES;
}

如果我们想要知道segue的ID, 源VC, 目标VC, 可以实现以下方法:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    
    NSLog(@"identifier:%@", segue.identifier);
    NSLog(@"sourceViewController:%@", segue.sourceViewController);
    NSLog(@"destinationViewController:%@", segue.destinationViewController);
}

UINavigationBar & UIToolBar

在了解UINavigationBar和UIToolBar之前, 我们先来看看UINavigationController、UIViewController、UINavigationBar、UIToolBar、UINavigationItem、toolbarItems之间的关系:

  1. UINavigationController用于管理多个UIViewController, 将UIViewController的view添加到content中显示. 也就是说, 多个UIViewController共用同一个UINavigationController.
  2. UINavigationBar、UIToolBar均是UINavigationController的属性. 也就是说, 多个UIViewController共用同一个UINavigationController、UINavigationBar、UIToolBar.
  3. UINavigationItem是UIViewController的属性, 用于管理标题title, 左边按钮(leftBarButtonItems), 右边按钮(rightBarButtonItems)等. 每个UIViewController的UINavigationItem均不同, 但其管理的控件都显示在UINavigationBar上面.
  4. 类似的, toolbarItems是UIViewController的属性, 用于管理底部的按钮. 每个UIViewController的toolbarItems均不同, 但其管理的控件都显示在UIToolBar上面.
  5. UINavigationBar、UIToolBar主要用于设置"全局变量", 比如位置布局, 主体颜色, 字体大小等等; UINavigationItem、toolbarItems则是每个UIViewController不同的属性, 用于设置VC各自的标题, 按钮功能等.

对照图片看效果更佳:

结构2.0

示例代码:

    /* --- navigationController属性 --- */
    
    // 点击隐藏navigationBar和toolbar, 再次点击显示
    self.navigationController.hidesBarsOnTap = YES;
    // 向上轻扫隐藏, 向下轻扫显示
    self.navigationController.hidesBarsOnSwipe = YES;
    // 键盘出现隐藏, 键盘消失仍隐藏, 可点击显示
    self.navigationController.hidesBarsWhenKeyboardAppears = YES;
    // 如果自定义了返回按键, 则滑动返回失能, 使用这行代码继续使能滑动返回
    self.navigationController.interactivePopGestureRecognizer.delegate = nil;

    /* --- navigationBar属性 --- */
    
    // 导航栏样式
    self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
    // 字体颜色
    self.navigationController.navigationBar.tintColor = [UIColor cyanColor];
    // 背景view颜色
    self.navigationController.navigationBar.barTintColor = [UIColor purpleColor];
    // 设置背景不透明
    self.navigationController.navigationBar.translucent = NO;
    // title样式
    NSShadow *shadow = [[NSShadow alloc] init];
    shadow.shadowOffset = CGSizeMake(2.0, 2.0);
    shadow.shadowColor = [UIColor grayColor];
    NSDictionary *titleTextAttributes = @{NSFontAttributeName               : [UIFont boldSystemFontOfSize:20],     // 类型、大小
                                          NSForegroundColorAttributeName    : [UIColor redColor],                   // 颜色
                                          NSShadowAttributeName             : shadow};                              // 阴影
    self.navigationController.navigationBar.titleTextAttributes = titleTextAttributes;
    
    /* --- toolbar属性 --- */
    
    // 工具栏样式
    self.navigationController.toolbar.barStyle = UIBarStyleBlack;
    // 字体颜色
    self.navigationController.toolbar.tintColor = [UIColor whiteColor];
    // 背景view颜色
    self.navigationController.toolbar.barTintColor = [UIColor brownColor];
    
    /* --- 当前viewController属性 --- */
    
    // title内容
    self.navigationItem.title = @"VC2 Title";
    self.title = @"VC2";
    NSLog(@"title:%p, nvItemTitle:%p", self.title, self.navigationItem.title);  // 结论: 这俩是同一个对象
    // 左边按钮 leftBarButtonItems
    UIBarButtonItem *lBarBtnItem1 = [[UIBarButtonItem alloc] initWithTitle:@"lBtn1" style:UIBarButtonItemStylePlain target:self action:@selector(lAction1)];
    UIBarButtonItem *lBarBtnItem2 = [[UIBarButtonItem alloc] initWithTitle:@"lBtn2" style:UIBarButtonItemStylePlain target:self action:@selector(lAction2)];
    self.navigationItem.leftBarButtonItems = @[lBarBtnItem1, lBarBtnItem2];
    // 右边按钮 rightBarButtonItems
    UIBarButtonItem *barBtnItem1 = [[UIBarButtonItem alloc] initWithTitle:@"rBtn1" style:UIBarButtonItemStylePlain target:self action:@selector(rAction1)];
    UIBarButtonItem *barBtnItem2 = [[UIBarButtonItem alloc] initWithTitle:@"rBtn2" style:UIBarButtonItemStylePlain target:self action:@selector(rAction2)];
    self.navigationItem.rightBarButtonItems = @[barBtnItem1, barBtnItem2];
    // 底部按钮 toolbarItems
    UIBarButtonItem *bBarBtnItem1 = [[UIBarButtonItem alloc] initWithTitle:@"bBtn1" style:UIBarButtonItemStylePlain target:self action:@selector(bAction1)];
    UIBarButtonItem *bBarBtnItem2 = [[UIBarButtonItem alloc] initWithTitle:@"bBtn2" style:UIBarButtonItemStylePlain target:self action:@selector(bAction2)];
    UIBarButtonItem *bBarBtnItem3 = [[UIBarButtonItem alloc] initWithTitle:@"bBtn3" style:UIBarButtonItemStylePlain target:self action:@selector(bAction3)];
    UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    self.toolbarItems = @[spaceItem, bBarBtnItem1, spaceItem, bBarBtnItem2, spaceItem, bBarBtnItem3, spaceItem];    // spaceItem自动算出空格区间

其他方法属性

topViewController & visibleViewController
UINavigationController有个visibleViewController属性, 即当前正在显示的VC. 这个VC可以是push进来或者present进来的, 如果是push进来的, 那么此VC同时也是topViewController; 如果是present进来的, 那么topViewController不等于visibleViewController.

代理方法

// 即将展示视图控制器时调用
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;

// 已经展示视图控制器时调用
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;

// 屏幕旋转时,navigationController 支持的方向,多选
- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;

/** 子控制器支持的方向
 * UIInterfaceOrientation 枚举类型
 *  1. UIInterfaceOrientationUnknown 设备的朝向不能确定。
 *  2. UIInterfaceOrientationPortrait  该设备处于竖屏模式,设备保持直立,底部的Home键。
 *  3. UIInterfaceOrientationPortraitUpsideDown 该设备处于竖屏模式,但上下颠倒,设备保持直立,顶部的Home键。
 *  4. UIInterfaceOrientationLandscapeLeft 设备处于横向模式,设备保持直立,右侧Home键。
 *  5. UIInterfaceOrientationLandscapeRight 该设备处于横向模式,设备保持直立,左侧Home键。
 */
- (UIInterfaceOrientation)navigationControllerPreferredInterfaceOrientationForPresentation:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;

返回按键

法一

修改系统返回按键的image, 不能添加文字

// 这两个必须同时设置
self.navigationBar.backIndicatorImage = image;
self.navigationBar.backIndicatorTransitionMaskImage = image;

监听VC退出(点击系统返回按键/pop操作)

- (void)viewWillDisappear:(BOOL)animated{
    
    // 判断 点击系统返回按键/pop操作
    if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound){
        NSLog(@"%s", __func__);
    }
    
    [super viewWillDisappear:animated];
}

自定义返回按钮, 原系统返回按钮消失

/*法1*/
UIBarButtonItem *backBarBtnItem1 = [[UIBarButtonItem alloc] initWithTitle:@"back" style:UIBarButtonItemStylePlain target:self action:@selector(backAction)];
self.navigationItem.backBarButtonItem = backBarBtnItem1;
self.navigationController.interactivePopGestureRecognizer.delegate = nil;  // 使能向右滑动返回

/*法2*/
UIButton *back = [UIButton buttonWithType:UIButtonTypeCustom];  
back.titleLabel.font = [UIFont boldSystemFontOfSize:13]; 
[back setTitle:@"Back" forState:UIControlStateNormal];  
[back setFrame:CGRectMake(0, 0, 50, 30)];  
[back addTarget:self action:@selector(backAction) forControlEvents:UIControlEventTouchUpInside];  
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:back];  
self.navigationItem.leftBarButtonItem = barButton; 
self.navigationController.interactivePopGestureRecognizer.delegate = nil;   // 使能向右滑动返回

法二

摘自https://github.com/onegray/UIViewController-BackButtonHandler

新建UIViewController 的 category.
.h文件

#import 

@protocol BackButtonHandlerProtocol 
@optional
// Override this method in UIViewController derived class to handle 'Back' button click
- (BOOL)navigationShouldPopOnBackButton;
@end

@interface UIViewController (BackButtonHandler) 

@end

.m文件 (已更新适配iOS13, 详情issues/13)

#import "UIViewController+BackButtonHandler.h"
#import 

@implementation UIViewController (BackButtonHandler)

@end

@implementation UINavigationController (ShouldPopOnBackButton)

+ (void)load {
    Method originalMethod = class_getInstanceMethod([self class], @selector(navigationBar:shouldPopItem:));
    Method overloadingMethod = class_getInstanceMethod([self class], @selector(overloaded_navigationBar:shouldPopItem:));
    method_setImplementation(originalMethod, method_getImplementation(overloadingMethod));
}

- (BOOL)overloaded_navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

    if([self.viewControllers count] < [navigationBar.items count]) {
        return YES;
    }

    BOOL shouldPop = YES;
    UIViewController* vc = [self topViewController];
    if([vc respondsToSelector:@selector(navigationShouldPopOnBackButton)]) {
        shouldPop = [vc navigationShouldPopOnBackButton];
    }

    if(shouldPop) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self popViewControllerAnimated:YES];
        });
    } else {
        // Workaround for iOS7.1. Thanks to @boliva - http://stackoverflow.com/posts/comments/34452906
        for(UIView *subview in [navigationBar subviews]) {
            if(0. < subview.alpha && subview.alpha < 1.) {
                [UIView animateWithDuration:.25 animations:^{
                    subview.alpha = 1.;
                }];
            }
        }
    }

    return NO;
}

@end

在用到的VC里面导入, 然后重写方法

#import "UIViewController+BackButtonHandler.h"

- (BOOL)navigationShouldPopOnBackButton {
    return NO;  // YES or NO
}

你可能感兴趣的:(iOS开发之UI篇(14)—— UINavigationController)