iOS 页面切换控制

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

iOS的页面基本由UIViewController, UINavigationController完成,切换方式也基本是Present, Push,Pop等等。这些切换过程会遇到以下两种crash

1. Can't add self as subview
2. Attempt to present xx  on yy  whose view is not in the window hierarchy!
3. 快速点击两次按钮,连续push两三次

第一个问题的原因有两种,一种是[self addSubview:self]; 第二种是连续两次push,或者pop,这个在iOS 7下概率极高。
第二个问题由于当前的页面根本不在window的最顶层,你无法使用当前VC做操作。

问题的根源在于系统在做UI切换的时候,由于动画没有执行完毕,页面层级和状态都不正确,这个时候再次发起切换动画,就会造成紊乱,严重时会引发crash

针对这两个问题

延时解决方案
self.navigationController?.pushViewController(UIViewController(), animated: true)
//延迟执行
DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 0.3)) {
     self.navigationController?.pushViewController(UIViewController(), animated: true)
}

弊端
这种延时的方法,偶尔用上一两处,可能可以解决问题,但是如果满大街都是这种使用方法,那么问题依然存在,因为大家都用延时,在一个时间线上,肯定会出现两个切换间隔时间不足的问题。

viewDidAppear增加变量控制
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated);
        //增加切换控制
    self.isAnimated = NO;
}
public func pushViewController(_ viewController:UIViewController, animated:Bool) {
    if (self.topViewController.isAnimated == ture) {
         return
    }
//省略
}
弊端

1.每一个VC增加一个类似的变量,需要在基类中维护
2.viewDidload被调用了,也不代表能push或者pop完成,真正完成切换在navigationController的代理didShow函数里面
3.没有做VC是否在Window 检测,依然会导致Crash

终极解决方案,UIWindow控制

问题的本质在于,同一时间一个UIWindow只能有一个页面切换,那么我们索性给UIWindow上增加一个控制变量

extension UIWindow {

    //动画标志状态
    var isAnimated: Bool {
        get{
            if objc_getAssociatedObject(self, &isAnimatedKey) != nil {
                return (objc_getAssociatedObject(self, &isAnimatedKey) as? Bool)!
            }
            return false
        }
        set{
            objc_setAssociatedObject(self, &isAnimatedKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
        }
    }
}

在每次切换的 加锁 变量, 弄完之后 释放 变量
切换前的需要处理的方法如下
UINavigationController,push pop, popTo 等等,先用方法替换的方式,
UIViewController做present 和dismiss方法处
将UINavigationController的方法做替换,这样可以便于

//UINavigationController
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "pushViewController:animated:", currentMethodName: "skipControlPushViewController:animated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popViewControllerAnimated:", currentMethodName: "skipControlPopViewControllerAnimated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popToViewController:animated:", currentMethodName: "skipControlPopToViewController:animated:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "popToRootViewControllerAnimated:", currentMethodName: "skipControlPopToRootViewControllerAnimated:")
 //present push 
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "presentViewController:animated:completion:", currentMethodName: "skipControlPresentViewController:animated:completion:")
UISkipControlHelper.exchangeMethod(className: self, originMethodName: "dismissViewControllerAnimated:completion:", currentMethodName: "skipControlDismissViewControllerAnimated:completion:")

然后我们在替换的方法中将具体的跳转转接到一个单例中去,让他去负责做 加锁 和释放window 操作,然后跳转,例如在pushViewController中做法如下

public func skipControlPushViewController(_ viewController:UIViewController, animated:Bool) {
// 由于UINavigationController initWithRootViewController 会调用该方法,并且当时没有显示在Window上,所以特殊处理,此处不加控制,并不会造成crash
   if  self.viewControllers.count == 0 && animated == false {
       self.skipControlPushViewController(viewController, animated: animated)
       return
   }

具体的 加锁 和释放Window的地方我们用了一个单例,而没有在push发发中执行 window. isAnimated = true? 先看看我们释放的window的地方在哪里,就明白为什么不这样做。
UINavigationController 切换完成时会给其代理函数发送一个一个方法

//切换前
optional public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)

//切换后 1 
optional public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)

//切换后2 发送

post  UINavigationControllerDidShowViewControllerNotification

切换之前会想代理调用willShow回调,切换后会调用didShow,切换后也会发送UINavigationControllerDidShowViewControllerNotification 这个通知。如果我们把释放和加锁放入UINavigationController扩展里面,势必要会将UINavigationController.delegate变更成UINavigationController本身,或者添加这个UINavigationControllerDidShowViewControllerNotification这个通知,那我们必然面临两个问题
- 1.由于delegate被这个扩展使用,而其他真正使用代理做事情的类,无法再次使用代理
- 2.我们添加了这个通知, 那么什么时候释放它(ios 9以上,不需要释放通知),没有合适的地方。

所以我们形成了一个单例去在程序整个生命周期去管理这个事务,我们采用检测通知的方式。
单例中的跳转处理方法如下

 func skipViewController(_ skipingController: UIViewController, skippedController: UIViewController?, skipType:UISkipControlSkipType, isAllowQueued:Bool, isAnimated:Bool, completionBlock:(()->Void)?) -> [AnyObject]? {
        /// 合法性检测,VC对应的Window必须存在
       weak var weakWindow = UIWindow.windowForViewController(skipingController)
        if weakWindow == nil {
            return nil
        }
        /// 构造切换完成后的清理工作
        weak var weakSkippingController = skipingController
        weak var weakSkippedController = skippedController
        let freeCompetionBlock = {
            //打印log
            let strongSkippingController = weakSkippingController
            let strongSkippedController = weakSkippedController
            print("DID -- \(strongSkippingController) \(skipType.rawValue) \(strongSkippedController)")
            //1. 切换完成后释放VC对应的Window的动画属性
            let strongWindow = weakWindow
            if (strongWindow != nil) {
                strongWindow?.isAnimated = false
            }
            //将(1)和(2)加入到主线程队列中执行,主要目的在于让系统完成自己的清场任务后执行,否则有问题
           // DispatchQueue.main.async {
                //(1). 执行自定义的完成切换回调
                if (completionBlock != nil) {
                    completionBlock!()
                }
                //(2). 执行该VC Window对应的队列
                strongWindow?.performAnimationBlock()
            //}
        }
        2.判断当前Window是否可以执行VC切换
        if weakWindow != nil && weakWindow?.isAnimated == false {
            //可以执行切换,先锁定window
            weakWindow?.isAnimated = true;
            //log
            print("WILL -- \(skipingController) \(skipType.rawValue) \(skippedController)")
            //执行切换
            return self.performSkip(skipingController, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated , completionBlock: freeCompetionBlock)
        } else if (isAllowQueued){ 
            //3.当前不能执行切换,但在允许加入队列的情况下,构造队列完成操作任务,加入到window队列
            weak var weakSelf = self
            weakWindow?.enqueueAnimationBlock {
                let strongSelf = weakSelf
                let strongSkippingController = weakSkippingController
                //let strongSkippedController = weakSkippedController 取消对 skippedController weak持有,否则push popTo present 无法执行
                if (strongSelf != nil && strongSkippingController != nil) {
                    //执行切换
                    strongSelf?.performSkip(strongSkippingController!, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated, completionBlock: freeCompetionBlock)
                }
            }

            //log
            print("QUEUED -- \(skipingController) \(skipType.rawValue) \(skippedController)")
        }
        //log 当前无法进行切换
        print("FAILED -- \(skipingController) \(skipType.rawValue) \(skippedController)")
        return nil
    }

代码稍微发杂了一点,原因是上面代码还考虑了另外一个需求,有时候我们冷启动Push,这个时候需要跳转,由于根本没有准备充足,直接跳转可能被阻挡,强制跳转可能会引起crash,所以我们增加了一个队列,捆绑在window上

fileprivate var skipAnimationQueue:[BlockObject]{
     get {
            if objc_getAssociatedObject(self, &skipAnimationQueueKey) != nil {
                return objc_getAssociatedObject(self, &skipAnimationQueueKey) as! [BlockObject]
            } else {
                let queue:[BlockObject] = [BlockObject]()
                self.skipAnimationQueue = queue;
                return queue;
            }
        }
        set {
            objc_setAssociatedObject(self, &skipAnimationQueueKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    //队列执行函数
    func performAnimationBlock()
    {
        if self.isAnimated == false {
            if self.skipAnimationQueue.count > 0 {
                let blockObject = self.skipAnimationQueue.first
                if blockObject != nil {
                    blockObject!.performBlock()
                    self.skipAnimationQueue.removeFirst()
                }
            }
        }
    }

    //加入队列
    public func enqueueAnimationBlock(_ block:@escaping ()->()){
        self.skipAnimationQueue.append(BlockObject(block: block))
    }

在widow上增加了一个block数组作为队列。可以将某一次跳转加入到队列中,这样就能保证每次跳转都是有次序的。
现在看最上面的代码就不难理解,我们做了以下的事情
1.构建一个切换完成的 freeBlock ,来处理完成后的window释放和原本用户添加的完成块,最后队列,查看队列是否有切换需要执行,这个freeblock会绑定到UINavigationController上
2.判断当前window是否可以执行动画,如果可以就直接执行具体切换,

 return self.performSkip(skipingController, skippedController: skippedController, skipType: skipType, isAnimated: isAnimated , completionBlock: freeCompetionBlock)

3.如果不能,查看调用是否有入队列的需求,有的话,加入UIWindwow的队列。
最后我们在接受通知的函数里面执freeblock

@objc public func handleNavigationControllerDidSkip(_ notification:NSNotification) {
    var navigationtroller:UINavigationController?
    if (notification.object != nil && notification.object is UINavigationController ) {
       navigationtroller = notification.object as! UINavigationController?
       if ((navigationtroller?.completionBlock) != nil) {
           navigationtroller?.completionBlock!()
       }
    }
}

最后我们使用起来如下

let vc = UIViewController() self.navigationController?.pushViewController(vc, animated: true);
let vc1 = UIViewController()
self.navigationController?.pushViewController(vc, animated: true, allowQueued: true, completionBlock: nil) //成功,因为加入到队列了

普通的跳转和原来的系统的api一样不会有任何变化,如果需要加入对垒可以使用体用的新函数。
全部结束,关于Present,和push类似,文章最后又源码地址。

总结

我们队切换控制增加了三点
- 给UIWindow增加一个变量保证同一时间只有一个切换
- 增加一个单例来控制切换,解放出了UINavigationController的delegate的真正用途
- 增加了跳转队列,避免了有些业务跳转一定要保证完成,而不是window不能执行时丢弃该操作

github源码地址:UISkipControl

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值