屏幕边缘侧滑返回手势 UIScreenEdgePanGestureRecognizer

由于iPhone不像安卓手机那样底部有返回键可以点击返回上个页面,每次返回上一页都需要点击页面的导航栏返回按钮,这实在是非常不方便。。。

还好苹果提供了屏幕边缘侧滑返回手势interactivePopGestureRecognizer,在屏幕左侧边缘轻轻一划就OK了,很方便,因此这个功能对APP来说是必选的,如果没有,那这个APP估计离大限也不远了。。。😨

给大家看下我的 新(mo) iPhone(ni) X(qi) 的效果😭


image.gif
该文章主要讲解:
  • 导航控制器的封装
  • 结合左滑抽屉手势的使用场景
  • 侧滑手势的添加
  • 页面越级返回问题
  • 真假导航栏页面之间的侧滑返回
  • 导航栏标题水平位置偏移、抖动、文字变淡等问题
  • 解决scrollView滚动和边缘侧滑手势冲突问题
  • 侧滑手势的禁用、启用
  • 侧滑手势pop页面事件拦截方法

封装导航控制器

由于本项目是tabBar的结构,并且首页还有一个左滑抽屉,所以我自定义一个导航控制器类NavigationController,好处就是:

  1. 统一整个项目的导航栏样式,如颜色、标题字体、返回按钮等
  2. 统一管理左滑抽屉手势的打开与关闭
  3. 方便集成边缘侧滑手势的集成

初始时,进行了除侧滑手势之外的配置,注释写的比较详细,就不多说了,请看下面的代码:

class NavigationController: UINavigationController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 导航栏样式
        navigationBar.isTranslucent = false
        navigationBar.barTintColor = UIColor.Main
        navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.auto(size: 18)]
    }
    
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        
        if viewControllers.count > 0 {
            // 隐藏tabBar底部
            viewController.hidesBottomBarWhenPushed = true
            // 关闭首页左侧抽屉手势
            drawerMenuController()?.needSwipeShowMenu = false
            //设置统一的返回按钮
            let backBtn = BackButton(target: self, action: #selector(backBtnClicked))
            viewController.navigationItem.leftBarButtonItem = UIBarButtonItem.init(customView: backBtn)
        }
        
        super.pushViewController(viewController, animated: animated)
    }
    
    // 导航栏返回按钮点击
    @objc fileprivate func backBtnClicked() {
        
        popViewController(animated: true)
        
        if viewControllers.count == 1 { // 打开首页左侧抽屉手势
            drawerMenuController()?.needSwipeShowMenu = true
        }
    }
}

侧滑手势添加

  • 1.实现代理

我们要用到的侧滑手势,其实就是UINavigationController的一个属性:interactivePopGestureRecognizer,后面我们要通过它的代理方法以及导航控制器的代理方法进行一些操作,
所以我们要先遵守这两个代理:UINavigationControllerDelegate, UIGestureRecognizerDelegate

class NavigationController: UINavigationController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.interactivePopGestureRecognizer?.delegate = self
        self.delegate = self
    }
}

这时push几个控制器后,滑动左侧边缘就会发现已经可以成功侧滑返回上一页了~

image1.gif
  • 2.实现didShow viewController方法(首页没有左侧抽屉菜单的可以跳过这一步)

接下来在首页像往常一样右滑,却发现左侧抽屉菜单怎么也呼唤不出来了。。。这是因为我们之前在push方法中关闭了抽屉菜单手势,然后在点击导航栏返回按钮回到首页时打开左侧抽屉手势,而现在由于是通过侧滑手势返回的,所以并没有触发按钮点击方法。

因此我们需要捕获通过侧滑手势返回页面后的事件,然后对抽屉菜单手势进行管理,这就用到了导航控制器的代理方法didShow viewController,法如其名,就是当控制器show的时候会就回调该方法,无论是push还是pop亦或者是切换后的appear。

// 捕获侧滑手势返回页面后的事件
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
    
    if viewControllers.count == 1 { // 回到首页,打开左侧抽屉手势
        drawerMenuController()?.needSwipeShowMenu = true
    }
}

这样就能愉快的在几个首页页面右滑呼唤出抽屉菜单了。

  • 3.实现gestureRecognizerShouldBegin方法

如果在首页的左侧边缘尝试滑动几下的话,就会出现页面卡死的情况,点什么都没有反应了(由于首页有抽屉菜单手势的原因所以不好触发,建议先把抽屉菜单手势关闭掉,然后在首页侧滑返回一下,一下就能触发!)

从网上搜了一下,发现是因为如果根控制器使用侧滑返回的话,UI界面就会变为假死状态,所以我们要使其在根控制器的时候不响应侧滑返回手势。

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    
    return childViewControllers.count > 1
}

至此,对于最基本常规的情况就实现完成了,但是对于一些非常规情况就不完美了,下面说一下其他需求场景~


其他需求场景中遇到的问题

一:页面越级返回问题

从A->B->C,然后从C跳过B直接返回A的解决方法:
侧滑手势页面越级返回问题

二:真假导航栏页面之间的侧滑返回

我们的需求经常在很多时候设计中间的某个页面的导航栏是不同于统一的样式的,对于这种情况最常见的做法就是自己绘制一个假的NavigationBar,然后在该页面出现的时候隐藏导航栏,在页面消失的时候再显示出导航栏,如下:

H1Controller.swift

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.isNavigationBarHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.isNavigationBarHidden = false
}

当侧滑时导航栏会一下子突然变成上一个页面的导航栏样式,并且后一半偶尔还会出现了红色(左滑抽屉的背景颜色),蛋蛋的忧伤啊😔。。。

image3.gif

对于这种情况,解决办法就是不要直接设置isNavigationBarHidden为true或false,而是通过调用带有animated的set方法去设置:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.setNavigationBarHidden(false, animated: animated)
}

这样就比较完美了~

image4.gif
三:导航栏标题水平位置偏移、抖动、文字变淡等问题

之前运行的时候遇到过侧滑返回时导航栏视图、标题文字一抖动的现象,除此之外文字还会偶现偏移、变淡等问题,效果如下:

image1.gif
image4.gif

这让我很是郁闷,直到那次抱着试试看的想法连上了真机。。。😭

我才发现,这些问题都不是问题,这全都是因为模拟器的bug所导致的,在真机上并没有这个问题!!!

刚刚我安装了Xcode9,用新模拟器运行了一下,发现这个问题已经没有了,苹果已经修复了这个bug~

四:解决scrollView滚动和边缘侧滑手势冲突问题

有两个方案:

  • 自定义一个UIScrollView类,重写gestureRecognizerShouldBegin方法,然后让控制器中的scrollView继承该类
  • 获取ScreenEdgePanGestureRecognizer手势,再通过panGestureRecognizer.require(toFail:)方法设置手势优先级
方案1:
/// 解决滚动视图跟页面的其他滑动手势(PanGuesture)冲突问题
class RecognizerScrollView: UIScrollView {
    
    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        
        if gestureRecognizer.isKind(of: UIPanGestureRecognizer.classForCoder()) {
            let pan = gestureRecognizer as? UIPanGestureRecognizer
            if ((pan?.translation(in: self).x) ?? 0 > CGFloat(0)) && (self.contentOffset.x == CGFloat(0)) {
                return false
            }
        }
        return super.gestureRecognizerShouldBegin(gestureRecognizer)
    }
}
方案2:

NavigationController.swift 文件

/// 获取屏幕边缘侧滑手势
func getScreenEdgePanGestureRecognizer() -> UIScreenEdgePanGestureRecognizer? {
    
    var edgePan: UIScreenEdgePanGestureRecognizer?
    if let recognizers = view.gestureRecognizers, recognizers.count > 0 {
        for recognizer in recognizers {
            if recognizer is UIScreenEdgePanGestureRecognizer {
                edgePan = recognizer as? UIScreenEdgePanGestureRecognizer
                break
            }
        }
    }
    return edgePan
}

带有scrollView的Controller.swift文件

// 解决scrollView滑动手势和屏幕边缘侧滑手势冲突问题
let navController = navigationController as? NavigationController
if let edgePan = navController?.getScreenEdgePanGestureRecognizer() {
    scrollView.panGestureRecognizer.require(toFail: edgePan)
}
五:在某个页面禁用侧滑手势

有时由于一些特殊的需求,可能需要控制某个页面不让侧滑手势返回,这就需要禁用、启用侧滑手势了。

NavigationController.swift 文件

fileprivate var isEnableEdegePan = true

/// 启用、禁用屏幕边缘侧滑手势
func enableScreenEdgePanGestureRecognizer(_ isEnable: Bool) {
    isEnableEdegePan = isEnable
}

// 更新该方法的判断
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {

    if !isEnableEdegePan { // 禁用边缘侧滑手势
        return false
    }
    return childViewControllers.count > 1
}

S1Controller.swift文件

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // 禁用屏幕边缘侧滑手势
    let navController = navigationController as? NavigationController
    navController?.enableScreenEdgePanGestureRecognizer(false)
}
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    // 启用屏幕边缘侧滑手势
    let navController = navigationController as? NavigationController
    navController?.enableScreenEdgePanGestureRecognizer(true)
}
六:在返回页面时除了pop之外,还需要其他操作

返回页面现在有两种途径:点击导航栏返回按钮 或 侧滑返回

1)点击导航栏返回

由于封装了导航栏的返回按钮,所以需要在需要的控制器中重写leftBarButtonItem,然后自定义事件。

override func viewDidLoad() {
    super.viewDidLoad()
    // 自定义返回按钮
    let backBtn = BackButton(target: self, action: #selector(backBtnClicked))
    navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backBtn)
}
2)侧滑手势返回

可以通过重写didMove(toParentViewController parent: UIViewController?)方法来监听控制器的pop操作。

该方法在push、pop、被父控制器add或者remove时都会调用,但是在pop时parent参数为空,所以可以根据parent为空来截取pop事件。

class S2Controller: UIViewController {

    override func didMove(toParentViewController parent: UIViewController?) {
        if parent == nil {
            print("S2---Pop")
            task()
        }
    }
}


以上就是我在项目实际开发中遇到的一些问题的总结,目前应该我们项目的需求已经足够了,后续如果有新的需求问题再补充。。。

具体场景的详细使用,请看:

github代码demo


昨天苹果发布会开完了,其实我感觉买不买iPhoneX根本不重要,日子过的开心就好,人不能活的太虚荣。🙂
(来自iPhone X) 📍青岛市人民医院肾脏摘除科住院部666床

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 已记不清从哪里说到眉毛。然后姐姐进门跟我说,眉毛淡的人薄情寡义。她好像在自言自语一般。薄情寡义,往往都在说命理。就...
    有丫阅读 3,440评论 0 0
  • “一百块一台电脑,晚上给我带来”朋友打电话给我。我一下子怔住了。 原来是省创强工作组来了,来验收学校的创强,其中...
    华先生阅读 146评论 0 0
  • 这个用户模式也是说到参与感的问题,只是更深入的解释参与感的实在意义。永远以客户为主,工作纪律都是围绕着用户来,工作...
    李宗翰阅读 133评论 0 0