iOS 導航欄控制 Tips

許久不寫UI,對UI的不少東西都生疏了,最近使用導航欄的各類場景作一些總結。git

1.導航欄的顯示與隱藏

導航欄的顯示與隱藏,分兩種狀況:github

  • 1.從不顯示導航欄的頁面push到顯示導航欄的頁面。
  • 2.從顯示導航欄的頁面Push到不顯示導航欄的頁面。

注意: 1.若是導航欄不顯示時,系統的側滑返回功能無效。 2.雖然側滑返回功能無效,可是導航欄的 .interactivePopGestureRecognizer.delegate仍是存在的。數組

針對以上兩種狀況分別處理,整個Push過程都假設是從A頁面跳轉到B頁面緩存

1.1 從不顯示導航欄的頁面Push到顯示導航欄的頁面。

關於導航欄的顯示,是否順滑,是經過以下兩個方法來控制。bash

// 不顯示動畫,導航欄顯示就比較突兀
[self.navigationController setNavigationBarHidden:YES];

// 顯示動畫,在側滑時,導航欄顯示就比較順滑
[self.navigationController setNavigationBarHidden:YES animated:YES];
複製代碼

因此,作法是: A頁面:app

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [self.navigationController setNavigationBarHidden:YES animated:YES];
}
複製代碼

B頁面:async

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [self.navigationController setNavigationBarHidden:NO animated:YES];
}
複製代碼

1.2 從顯示導航欄的頁面跳轉到不顯示導航欄的頁面

這種狀況的作法以下: A頁面:ide

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.navigationController setNavigationBarHidden:NO animated:YES];
}
複製代碼

B頁面:動畫

// 在頁面將要出現時,記錄原始側滑手勢代理對象,並將手勢代理設置爲當前頁面
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    self.interactivePopDelegate = self.navigationController.interactivePopGestureRecognizer.delegate;
    self.navigationController.interactivePopGestureRecognizer.delegate = self;
    
    [self.navigationController setNavigationBarHidden:YES animated:YES];
}

// 在頁面消失時,還原側滑手勢代理對象
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    
    self.navigationController.interactivePopGestureRecognizer.delegate = self.interactivePopDelegate;
    self.interactivePopDelegate = nil;
}

// 實現手勢代理,爲了防止影響其餘手勢,能夠判斷一下手勢類型
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]) {
        return YES;
    }
    ...... 其餘手勢的處理
    
    
    return NO;
}

複製代碼

2.統一重寫導航欄返回按鈕

有時候,咱們可能須要統一工程中的返回按鈕樣式,好比都是 箭頭+返回 或者都是 箭頭。 方案有兩種:ui

  • 1.建立一個BaseViewController,而後統一設置navigationItem.leftBarButtonItem。
  • 2.重寫導航控制器的Push方法,在push以前,設置navigationItem.backBarButtonItem

注意: 若是重寫了導航欄的leftBarButtonItem,那麼側滑返回功能也就失效了,須要側滑返回功能須要本身處理。

第一種方案比較簡單就不作贅述了,第二種方案是這樣的:

自定義導航控制器,而後重寫以下方法:

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStyleDone target:nil action:nil];
    viewController.navigationItem.backBarButtonItem = backItem;
    
    [super pushViewController:viewController animated:animated];
}
複製代碼

若是不須要返回這兩個字,只須要這樣寫就好。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:nil style:UIBarButtonItemStyleDone target:nil action:nil];
    viewController.navigationItem.backBarButtonItem = backItem;
    
    [super pushViewController:viewController animated:animated];
}
複製代碼

3.監聽返回按鈕的點擊事件

在有些場景,咱們須要監聽返回按鈕的事件。好比,當頁面用戶輸入了一些內容後,用戶要點擊返回,想要回到上一個頁面時,提醒用戶是否要緩存已經輸入的內容。

若是咱們重寫了導航欄的返回按鈕,那麼處理這種狀況就很Easy,不作贅述了。 可是,若是咱們沒有重寫過系統的返回按鈕,想要處理這種狀況就比較麻煩,可是也是能夠處理的。

處理步驟以下: 1.首先建立一個UIViewController的類別,頭文件(.h)的內容以下:

@protocol BackItemProtocol <NSObject>

- (BOOL)navigationShouldPopWhenBackButtonClick;

@end

@interface UIViewController (BackItem)<BackItemProtocol>

@end

@interface UINavigationController (BackItem)

@end
複製代碼

包含一個協議、UIViewController的類別、UINavigationController的類別。

而後,實現文件(.m)以下:

#import "UIViewController+BackItem.h"

@implementation UIViewController (BackItem)

- (BOOL)navigationShouldPopWhenBackButtonClick
{
    return YES;
}

@end


@implementation UINavigationController (BackItem)

// 這個實際上是導航欄的協議方法,在這裏重寫了
- (BOOL)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(navigationShouldPopWhenBackButtonClick)]) {
        shouldPop = [vc navigationShouldPopWhenBackButtonClick];
    }
    
    if (shouldPop) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self popViewControllerAnimated:YES];
        });
    } else {
        for(UIView *subview in [navigationBar subviews]) {
            if(subview.alpha < 1) {
                [UIView animateWithDuration:.25 animations:^{
                    subview.alpha = 1;
                }];
            }
        }
    }
    return NO;
}

@end
複製代碼

默認是,不須要處理返回按鈕的事件,直接使用系統的pop方法。

可是,若是咱們須要在用戶點擊返回按鈕時,彈窗提示,那就須要導入這個類別。 而後,重寫一個方法:

- (BOOL)navigationShouldPopWhenBackButtonClick
{
    BOOL isFlag = 輸入框不爲空等等條件
    if (isFlag) {
        UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:nil message:@"是否保存修改" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            // 這裏延時執行是由於UIAlertController阻塞UI,可能會致使動畫的不流暢
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self.navigationController popViewControllerAnimated:YES];
            });
        }];
        UIAlertAction *saveAction = [UIAlertAction actionWithTitle:@"保存" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        // 這裏延時執行是由於UIAlertController阻塞UI,可能會致使動畫的不流暢
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self rightClick];
            });
        }];
        [alertVC addAction:cancelAction];
        [alertVC addAction:saveAction];
        [self presentViewController:alertVC animated:YES completion:nil];
        return NO;
    }
    
    return YES;
}
複製代碼

4.導航控制器的頁面跳轉方式

安卓中的頁面跳轉有四種方式: standard、singleTop、singleTask、singleInstance。

例如singleTask,在作IM類App,跳轉到聊天室的場景,就很是有用,能夠保證控制器棧中只有一個聊天室,避免返回時層級太深。

iOS端若是要仿這個效果的話,能夠利用導航控制器的API:

- (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers animated:(BOOL)animated
複製代碼

首先,爲UINavigationController 建立一個類別。 好比:

UINavigationController+HLPushAndPop.h
UINavigationController+HLPushAndPop.m
複製代碼

而後,新增幾個方法:

拿兩個方法來舉例

- (void)hl_pushSingleViewController:(UIViewController *)viewController
                           animated:(BOOL)animated;

- (void)hl_pushSingleViewController:(UIViewController *)viewController
                         parentClass:(Class)parentClass
                            animated:(BOOL)animated;
複製代碼

再而後,實現方法:

實現步驟:

    1. 建立新的數組複製導航控制器原來的堆棧中的控制器。
    1. 在原始堆棧數組中判斷是否存在該類型的控制器,若是存在記錄其索引。
    1. 在複製的數組中將索引及上方全部控制器移除。
    1. 把將要push出來的控制器添加到複製的數組中。
    1. 將新的控制器數組設置爲導航控制器的棧數組,根據參數判斷是否要顯示動畫。

我這邊作了一些發散,由於一些類可能會有不少子類,那麼想要保證父類以及子類的實例都只有一個,因此將方法作了改進。

- (void)hl_pushSingleViewController:(UIViewController *)viewController
                           animated:(BOOL)animated
{
    [self hl_pushSingleViewController:viewController parentClass:viewController.class animated:animated];
}

- (void)hl_pushSingleViewController:(UIViewController *)viewController
                        parentClass:(Class)parentClass
                           animated:(BOOL)animated
{
    if (!viewController) {
        return;
    }
    // 若是要push的界面不是 parentClass以及其子類的實例,則按照方法1處理
    if (![viewController isKindOfClass:parentClass]) {
        [self hl_pushSingleViewController:viewController animated:animated];
        return;
    }
    
    // 判斷 導航控制器堆棧中是否有parentClass以及其子類的實例
    NSArray *childViewControllers = self.childViewControllers;
    NSMutableArray *newChildVCs = [[NSMutableArray alloc] initWithArray:childViewControllers];
    BOOL isExit = NO;
    NSInteger index = 0;
    for (int i = 0; i < childViewControllers.count; i++) {
        UIViewController *vc = childViewControllers[i];
        if ([vc isKindOfClass:parentClass]) {
            isExit = YES;
            index = i;
            break;
        }
    }
    
    // 若是不存在,則直接push
    if (!isExit) {
        [self pushViewController:viewController animated:animated];
        return;
    }
    
    // 若是存在,則將該實例及上面的全部界面所有彈出棧,而後將要push的界面放到棧頂。
    for (NSInteger i = childViewControllers.count - 1; i >= index; i--) {
        [newChildVCs removeObjectAtIndex:i];
    }
    
    [newChildVCs addObject:viewController];
    viewController.hidesBottomBarWhenPushed = (newChildVCs.count > 1);
    [self setViewControllers:newChildVCs animated:animated];
}

複製代碼

固然了,除了上面這些場景,還能夠擴展出一些其餘的場景,好比咱們指望將要push出來的控制器再某個棧中控制器的後面或者前面,這樣當點擊返回或者側滑時,就直接回到了指定頁面了。

或者咱們知道將要返回的頁面的類型,直接pop回指定頁面。

擴展出來的其餘方法都在Demo中了,有興趣的能夠看一下。

地址是:HLProject

相關文章
相關標籤/搜索