iOS小技能:右滑返回

語言: CN / TW / HK

攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第18天,點選檢視活動詳情

引言

原理:利用系統的返回手勢interactivePopGestureRecognizer進行實現

使用場景: 返回按鈕有點小,不好觸發返回時,可藉助右滑返回來提升使用者體驗

I 新增右滑返回手勢

1.1 基於全域性的UINavigationController基類實現

若專案有全域性的UINavigationController基類,給頁面新增右滑返回手勢

```objectivec @implementation NavigationController

  • (void)viewDidLoad { [super viewDidLoad]; //設定右滑返回手勢的代理為自身 __weak typeof(self) weakself = self; if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) { self.interactivePopGestureRecognizer.delegate = (id)weakself; } }

pragma mark - UIGestureRecognizerDelegate

//這個方法是在手勢將要啟用前呼叫:返回YES允許右滑手勢的啟用,返回NO不允許右滑手勢的啟用 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer == self.interactivePopGestureRecognizer) { //遮蔽呼叫rootViewController的滑動返回手勢,避免右滑返回手勢引起宕機問題 if (self.viewControllers.count < 2 || self.visibleViewController == [self.viewControllers objectAtIndex:0]) { return NO; } } //這裡就是非右滑手勢呼叫的方法啦,統一允許啟用 return YES; } ```

1.2 方法交換(推薦)

往UIViewController 新增forceEnableInteractivePopGestureRecognizer方法將手勢返回強制加回來

```objectivec @implementation UIViewController (ERPPresent13) + (void)load {

[self addMethod:self.class method:@selector(forceEnableInteractivePopGestureRecognizer) method:@selector(kunnan_forceEnableInteractivePopGestureRecognizer)];

}

  • (BOOL)kunnan_forceEnableInteractivePopGestureRecognizer {

    return YES;

}

```

II QMUI導致右滑返回沒有生效的解決方法

先來看看QMUI如何實現實現右滑返回?

2.1 問題分析

QMUI使用分類UINavigationController (QMUI)方式進行控制右滑返回,具體核心程式碼如下

  1. 重寫viewDidLoad設定右滑返回手勢的代理為自身 ```objectivec ExtendImplementationOfVoidMethodWithoutArguments([UINavigationController class], @selector(viewDidLoad), ^(UINavigationController *selfObject) { selfObject.qmui_interactivePopGestureRecognizerDelegate = selfObject.interactivePopGestureRecognizer.delegate; selfObject.interactivePopGestureRecognizer.delegate = (id)selfObject; });

``` 2. gestureRecognizerShouldBegin

這個方法是在手勢將要啟用前呼叫:返回YES允許右滑手勢的啟用,返回NO不允許右滑手勢的啟用

```objectivec - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer == self.interactivePopGestureRecognizer) { BOOL canPopViewController = [self canPopViewController:self.topViewController byPopGesture:YES]; if (canPopViewController) { if ([self.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { return [self.qmui_interactivePopGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer]; } else { return NO; } } else { return NO; } } return YES; }

```

  1. iOS 13.4 開始會優先詢問shouldReceiveEvent方法,只有返回 YES 後才會繼續後續的邏輯

```objectivec

  • (BOOL)_gestureRecognizer:(UIGestureRecognizer )gestureRecognizer shouldReceiveEvent:(UIEvent )event { if (gestureRecognizer == self.interactivePopGestureRecognizer) { NSObject *originGestureDelegate = self.qmui_interactivePopGestureRecognizerDelegate; if ([originGestureDelegate respondsToSelector:_cmd]) { BOOL originalValue = YES; [originGestureDelegate qmui_performSelector:_cmd withPrimitiveReturnValue:&originalValue arguments:&gestureRecognizer, &event, nil]; if (!originalValue && [self shouldForceEnableInteractivePopGestureRecognizer]) { return YES; } return originalValue; } } return YES; }

`` 其中在第三步中shouldForceEnableInteractivePopGestureRecognizer呼叫了UINavigationControllerBackButtonHandlerProtocol協議的forceEnableInteractivePopGestureRecognizer` 進行判定是否返回。 在這裡插入圖片描述

```objectivec - (BOOL)shouldForceEnableInteractivePopGestureRecognizer { UIViewController *viewController = [self topViewController]; return self.viewControllers.count > 1 && self.interactivePopGestureRecognizer.enabled && [viewController respondsToSelector:@selector(forceEnableInteractivePopGestureRecognizer)] && [viewController forceEnableInteractivePopGestureRecognizer]; }

```

當自定義了leftBarButtonItem按鈕之後,系統的手勢返回就失效了。

可以通過forceEnableInteractivePopGestureRecognizer來決定要不要把那個手勢返回強制加回來。當 interactivePopGestureRecognizer.enabled = NO 或者當前UINavigationController堆疊的viewControllers小於2的時候此方法無效。

自定義了leftBarButtonItem按鈕 ```objectivec - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{

viewController.hidesBottomBarWhenPushed = self.viewControllers.count == 1;


if (self.viewControllers.count>0) {

    [self setNavigationBarHidden:NO animated:NO];

// viewController.hidesBottomBarWhenPushed =YES; //設定左邊按鈕

    UIBarButtonItem *backItem      =nil;


    if ([viewController respondsToSelector:@selector(KNbackAction)]) {

         backItem =[[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:QCTNAVicon_left] style:0 target:viewController action:@selector(KNbackAction)];




    }else{

        backItem =[[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:QCTNAVicon_left] style:0 target:self action:@selector(backAction)];



    }

        viewController.navigationItem.leftBarButtonItems = @[backItem];








}
[super pushViewController:viewController animated:animated];

}

```

2.2 解決方法

所以當你自定義導航欄(自定義了leftBarButtonItem按鈕)沒采用系統的預設的實現,發生當前不可以手勢返回,可先檢查為什麼當前狀態,系統不允許你的手勢返回,例如是否隱藏了 navigationBar,或者隱藏了系統的返回按鈕?

比如push的時候,自定義了leftBarButtonItem按鈕了,你可以採用分類方式往UIViewController 新增forceEnableInteractivePopGestureRecognizer方法將手勢返回強制加回來

2.3 動態新增方法

使用場景:

  1. 在訊息傳送和訊息轉發時會用到動態新增方法
  2. 全域性控制返回手勢

下面的+addMethod方法有三個引數,第一個引數是要新增方法的類,第二個引數是方法的SEL,第三個引數則是提供方法實現的SEL。

使用class_getInstanceMethod()method_getImplementation()獲取相應SEL。

下方的IMP其實就是Implementation的方法縮寫,獲取到相應的方法實現後,然後再呼叫class_addMethod()方法將IMP與SEL進行繫結即可。 ```objectivec

/** 往類上新增新的方法與其實現

@param class 相應的類 @param methodSel 新增的方法 @param methodSelImpl 包含方法實現的SEL / + (void)addMethod:(Class)class method:(SEL)methodSel method:(SEL)methodSelImpl { Method method = class_getInstanceMethod(class, methodSelImpl); IMP methodIMP = method_getImplementation(method); const char types = method_getTypeEncoding(method); class_addMethod(class, methodSel, methodIMP, types); }

```

往UIViewController 新增forceEnableInteractivePopGestureRecognizer方法將手勢返回強制加回來

```objectivec @implementation UIViewController (ERPPresent13) + (void)load {

[self addMethod:self.class method:@selector(forceEnableInteractivePopGestureRecognizer) method:@selector(kunnan_forceEnableInteractivePopGestureRecognizer)];

}

  • (BOOL)kunnan_forceEnableInteractivePopGestureRecognizer {

    return YES;

}

```

III 相關知識點

3.1 WKWebView實現手勢左滑返回上一級

```objectivec // UI代理 _webView.UIDelegate = self; // 導航代理 _webView.navigationDelegate = self; // 是否允許手勢左滑返回上一級, 類似導航控制的左滑返回 _webView.allowsBackForwardNavigationGestures = YES; //可返回的頁面列表, 儲存已開啟過的網頁 WKBackForwardList * backForwardList = [_webView backForwardList];

```

完整初始化程式碼

```objectivec - (WKWebView *)webView{ if(_webView == nil){

    //建立網頁配置物件
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

    // 建立設定物件
    WKPreferences *preference = [[WKPreferences alloc]init];
    //最小字型大小 當將javaScriptEnabled屬性設定為NO時,可以看到明顯的效果
    preference.minimumFontSize = 0;
    //設定是否支援javaScript 預設是支援的
    preference.javaScriptEnabled = YES;
    // 在iOS上預設為NO,表示是否允許不經過使用者互動由javaScript自動開啟視窗
    preference.javaScriptCanOpenWindowsAutomatically = YES;
    config.preferences = preference;

    // 是使用h5的影片播放器線上播放, 還是使用原生播放器全屏播放
    config.allowsInlineMediaPlayback = YES;
    //設定影片是否需要使用者手動播放  設定為NO則會允許自動播放
    config.requiresUserActionForMediaPlayback = YES;
    //設定是否允許畫中畫技術 在特定裝置上有效
    config.allowsPictureInPictureMediaPlayback = YES;
    //設定請求的User-Agent資訊中應用程式名稱 iOS9後可用
    config.applicationNameForUserAgent = @"ChinaDailyForiPad";

    //自定義的WKScriptMessageHandler 是為了解決記憶體不釋放的問題
    WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
    //這個類主要用來做native與JavaScript的互動管理
    WKUserContentController * wkUController = [[WKUserContentController alloc] init];
    //註冊一個name為jsToOcNoPrams的js方法 設定處理接收JS方法的物件
    [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcNoPrams"];
    [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcWithPrams"];

    config.userContentController = wkUController;

    //以下程式碼適配文字大小
    NSString *jSString = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
    //用於進行JavaScript注入
    WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    [config.userContentController addUserScript:wkUScript];

    _webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];
    // UI代理
    _webView.UIDelegate = self;
    // 導航代理
    _webView.navigationDelegate = self;
    // 是否允許手勢左滑返回上一級, 類似導航控制的左滑返回
    _webView.allowsBackForwardNavigationGestures = YES;
    //可返回的頁面列表, 儲存已開啟過的網頁
    WKBackForwardList * backForwardList = [_webView backForwardList];



    NSString *path = [[NSBundle mainBundle] pathForResource:k_localHtml4csdn ofType:nil];
    NSString *htmlString = [[NSString alloc]initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [_webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]];

}
return _webView;

}

```

3.2 自定義導航條的rightBarButtonItem

自定義導航條的rightBarButtonItem,採用initWithCustomView:rightBtn設定rightBtn.frame,讓文字更大,更容易點選

https://kunnan.blog.csdn.net/article/details/77675855在這裡插入圖片描述

```objectivec -(void) setuprightBtn{

UIButton *rightBtn = [UIButton buttonWithType:UIButtonTypeCustom];
rightBtn.frame = CGRectMake(0, 0, 44, 44);

// [rightBtn setImage:[UIImage imageNamed:@"icon_shoukuan_shaixuan_n"] forState:UIControlStateNormal]; [rightBtn setTitle:@"編輯" forState:UIControlStateNormal];

[rightBtn setTitleColor:kcellColor forState:UIControlStateNormal];

[rightBtn addTarget:self action:@selector(gotoEditVC) forControlEvents:UIControlEventTouchUpInside];
[rightBtn setImageEdgeInsets:UIEdgeInsetsMake(0, 22, 0, 0)];

UIBarButtonItem *rightButtonItem = [[UIBarButtonItem alloc]initWithCustomView:rightBtn];


self.navigationItem.rightBarButtonItem = rightButtonItem;

self.navigationItem.rightBarButtonItem.customView.hidden = YES;

}

```

see also

iOS執行時API應用:1、實現路由(介面控制app跳任意介面 )2、獲取修改物件的成員屬性3、動態新增/交換方法的實現4、屬性關聯

https://kunnan.blog.csdn.net/article/details/112822138