天天看點

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

版本

Xcode 10.2

iPhone 6s (iOS12.4)

目錄

  • 版本
  • 繼承關系
  • 簡介
  • 建立
  • 切換視圖控制器
  • UINavigationBar & UIToolBar
  • 其他方法屬性
  • 傳回按鍵
    • 法一
    • 法二

繼承關系

UINavigationController : UIViewController : UIResponder UIResponder : NSObject
           

簡介

導航控制器是容器視圖控制器,其管理導航界面中的一個或多個子視圖控制器。在這種類型的界面中,一次隻能看到一個子視圖控制器。視圖控制器間的切換: 使用動畫在螢幕上推出新的視圖控制器,進而隐藏先前的視圖控制器。點選頂部導航欄中的後退按鈕将删除頂視圖控制器,進而顯示下方的視圖控制器。

以Apple自家的設定App為例, 點選Setting中的General會推出General界面, 點選Auto-Lock會推出Auto-Lock界面. 如下圖:

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

UINavigationController使用有序數組(稱為導航堆棧)管理其子視圖控制器。數組中的第一個視圖控制器是根視圖控制器 (沒有rootViewController屬性, 使用viewControllers[0]擷取),表示堆棧的底部。數組中的最後一個視圖控制器是堆棧中最頂層視圖控制器(topViewController)。我們可以使用segue或使用此類的方法從堆棧中添加和删除視圖控制器, 還可以使用導航欄中的後退按鈕或使用左邊滑動手勢來移除最頂層的視圖控制器。

結構

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

結構圖中主要有三個部分: 頂部的UINavigationBar, 底部預設隐藏的UIToolBar, 以及中間content部分存放子視圖控制器的view.

UINavigationController是一個容器視圖控制器 , 也就是說,它将其他視圖控制器的内容嵌入其自身内部。我們可以使用其view屬性通路導航控制器的視圖。

雖然導航欄和工具欄視圖的内容發生更改,但視圖本身是不變的。實際更改的唯一視圖是導航堆棧上最頂層視圖控制器提供的自定義内容視圖。

管理的對象

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

如圖, 導航控制器主要管理四個對象: 子視圖控制器, 導航欄, 工具欄, 其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, 會出現以下選項.

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

選擇show, 然後segue就形成了. 使用的時候點選A中的這個觸發子控件就可以跳轉至B了.

關于segue的幾種類型 (已經遺棄的類型不予讨論)

  1. Show

    對應代碼方法-showViewController:sender:

    将目标視圖控制器推到導航堆棧上,從右向左滑動,提供傳回按鈕. 如果未嵌入導航控制器,它将以模态方式顯示.

    示例:點選某一内容顯示另一個視圖界面鋪滿螢幕

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵
  1. Show Detail

    對應代碼方法-showDetailViewController:sender:

    用于拆分視圖控制器(UISplitViewController)時,在展開的2列界面中替換詳細/輔助視圖控制器,否則如果折疊為1列,則将推入導航控制器.

    示例:在"設定"中點選"通用"選項, iPhone推出完整界面鋪滿螢幕, iPad推出第2列界面.

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵
  1. Present Modally

    對應代碼方法-presentViewController:animated:completion:

    呈現出的各種帶動畫效果的視圖控制器,覆寫前一個視圖控制器. 在iPhone中, 新的VC從底部動畫向上彈出并覆寫整個螢幕; 在iPad上通常将新的VC顯示為居中的框,使原來的VC變暗.

    示例:在“設定”中選擇“觸摸ID和密碼”

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵
  1. Present As Popover

    對應代碼方法 iPad(-presentPopoverFromRect:inView:permittedArrowDirections:animated:), iPhone(-presentViewController:animated:completion:)

    在iPhone中,預設情況下新的VC會在整個螢幕上以模态方式顯示; 在iPad上運作時,新的VC以彈窗形式顯示在點選處的旁邊,點選此彈出框外的任何位置都會回收推出這個新VC.

    示例:點選月曆中的+按鈕

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵
  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中跳轉代碼如下:

如果我們綁定了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各自的标題, 按鈕功能等.

對照圖檔看效果更佳:

iOS開發之UI篇(14)—— UINavigationController版本繼承關系簡介建立切換視圖控制器UINavigationBar & UIToolBar其他方法屬性傳回按鍵

示例代碼:

/* --- 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 <UIKit/UIKit.h>

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

@interface UIViewController (BackButtonHandler) <BackButtonHandlerProtocol>

@end
           

.m檔案 (已更新适配iOS13, 詳情issues/13)

#import "UIViewController+BackButtonHandler.h"
#import <objc/runtime.h>

@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
}
           

繼續閱讀