来自 电脑系统 2019-09-15 04:19 的文章
当前位置: 金沙澳门官网网址 > 电脑系统 > 正文

【金沙澳门官网网址】iOS页面控制切换控制,自

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

上面第一个方法返回动画持续的时间,而下面这个方法才是具体需要实现动画的地方。UIViewControllerAnimatedTransitioning的协议都包含一个对象:transitionContext,通过这个对象能获取到切换时的上下文信息,比如从哪个VC切换到哪个VC等。我们从transitionContext获取containerView,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKeyUITransitionContextToViewControllerKey就是从哪个VC切换到哪个VC,容易理解;除此之外,还有直接获取view的UITransitionContextFromViewKeyUITransitionContextToViewKey等。我按Push和Pop把动画简单的区分了一下,Push时scale由小变大,Pop时scale由大变小,不同的操作,toViewController的视图层次也不一样。最后,在动画完成的时候调用completeTransition,告诉transitionContext你的动画已经结束,这是非常重要的方法,必须调用。在动画结束时没有对containerView的子视图进行清理(比如把fromViewController的view移除掉)是因为transitionContext会自动清理,所以我们无须在额外处理。

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

4、这样,只需在

注意一点,这样一来会发现原来导航栏的交互式返回效果没有了,如果你想用原来的交互式返回效果的话,在返回动画控制器的delegate方法里返回nil,如:

if operation == UINavigationControllerOperation.Push { navigationOperation = operation return self}return nil

然后在LSNavigationControllerviewDidLoad里,Objective-C直接self.navigationController.interactivePopGestureRecognizer.delegat = self就行了,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self之外,还要在self上声明实现了UIGestureRecognizerDelegate这个协议,虽然实际上你并没有实现。一个简单的自定义导航栏Push/Pop动画就完成了。

自定义Modal的Present与Dismiss动画与之前类似,都需要提供一个动画管理器,我们用详情页面来展示一个Modal页面,详情页面就作为动画管理器:为了方便,我依然在LSViewController操作(实际开发中,只需要在有特殊需要的页面中实现即可),1、 LSViewController实现协议UIViewControllerTransitioningDelegate,这个协议与之前的UINavigationControllerDelegate协议具有相似性,都是返回一个动画管理器,

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return LSPresentAnimation.init() } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return LSDismissAnimation.init() }

其中LSPresentAnimationLSDismissAnimation也是实现了UIViewControllerAnimatedTransitioning协议的用来实现具体的动画。直接上代码。

//UIViewControllerAnimatedTransitioning func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.6 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) var destView: UIView! var destTransfrom = CGAffineTransform() let screenHeight = UIScreen.main .bounds.size.height destView = toViewController?.view destView.transform = CGAffineTransform(translationX: 0, y: screenHeight) containerView.addSubview((toViewController?.view)!) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: UIViewAnimationOptions.curveLinear, animations: { destView.transform = destTransfrom }, completion: {completed in transitionContext.completeTransition }
终极解决方案,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的方法做替换,这样可以便于

//UINavigationControllerUISkipControlHelper.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 } //将加入到主线程队列中执行,主要目的在于让系统完成自己的清场任务后执行,否则有问题 // DispatchQueue.main.async { //. 执行自定义的完成切换回调 if (completionBlock != nil) { completionBlock!. 执行该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不能执行时丢弃该操作

swift github源码地址:UISkipControlobjective-c 源码地址 引用 UISkipControl 版本 0.0.1

3、在 LSViewControllerself.transitioningDelegate = self(之所以让viewControllersecondViewcontroller继承LSViewController是因为懒,都写在一起了,不然需要在两个VC中实现)

这样present就会有动画,想要dismiss也实现我们自己的动画需要在viewControllerpresent按钮中将 secondViewController定位动画管理器。

以上简单demo可以点击下载

与动画控制器类似,我们把实现了*** UIViewControllerInteractiveTransitioning 协议的对象称之为交互控制器***,最常用的就是把交互控制器应用到导航栏的Back手势返回上,而如果要实现一个自定义的交互式动画,我们有两种方式来完成:实现一个交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition类作交互控制器。

使用UIPercentDrivenInteractiveTransition

我们这里就用UIPercentDrivenInteractiveTransition来完成导航栏的交互式动画。先看下UIPercentDrivenInteractiveTransition的定义:

open class UIPercentDrivenInteractiveTransition : NSObject, UIViewControllerInteractiveTransitioning { /// This is the non-interactive duration that was returned when the /// animators transitionDuration: method was called when the transition started. open var duration: CGFloat { get } /// The last percentComplete value specified by updateInteractiveTransition: open var percentComplete: CGFloat { get } /// completionSpeed defaults to 1.0 which corresponds to a completion duration of /// (1 - percentComplete)*duration. It must be greater than 0.0. The actual /// completion is inversely proportional to the completionSpeed. This can be set /// before cancelInteractiveTransition or finishInteractiveTransition is called /// in order to speed up or slow down the non interactive part of the /// transition. open var completionSpeed: CGFloat /// When the interactive part of the transition has completed, this property can /// be set to indicate a different animation curve. It defaults to UIViewAnimationCurveEaseInOut. /// Note that during the interactive portion of the animation the timing curve is linear. open var completionCurve: UIViewAnimationCurve /// For an interruptible animator, one can specify a different timing curve provider to use when the /// transition is continued. This property is ignored if the animated transitioning object does not /// vend an interruptible animator. @available(iOS 10.0, *) open var timingCurve: UITimingCurveProvider? /// Set this to NO in order to start an interruptible transition non /// interactively. By default this is YES, which is consistent with the behavior /// before 10.0. @available(iOS 10.0, *) open var wantsInteractiveStart: Bool /// Use this method to pause a running interruptible animator. This will ensure that all blocks /// provided by a transition coordinator's notifyWhenInteractionChangesUsingBlock: method /// are executed when a transition moves in and out of an interactive mode. @available(iOS 10.0, *) open func pause() // These methods should be called by the gesture recognizer or some other logic // to drive the interaction. This style of interaction controller should only be // used with an animator that implements a CA style transition in the animator's // animateTransition: method. If this type of interaction controller is // specified, the animateTransition: method must ensure to call the // UIViewControllerTransitionParameters completeTransition: method. The other // interactive methods on UIViewControllerContextTransitioning should NOT be // called. If there is an interruptible animator, these methods will either scrub or continue // the transition in the forward or reverse directions. open func update(_ percentComplete: CGFloat) open func cancel() open func finish()}

实际上这个类就是实现了UIViewControllerInteractiveTransitioning协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用update更新进度;调用cancel取消交互,返回到切换前的状态;调用finish通知上下文交互已完成,同completeTransition一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controllerdelegate同一时间只能有一个。

首先我们需要创建一个交互控制器。新建一个Cocoa Touch Class文件,命名为LSPercentDrivenInteractiveTransition,让它继承自UIPercentDrivenInteractiveTransition

打开LSNavigationController.swift,在类定义的最开始添加下面这些属性:

var interactionInProgress = false //用于指示交互是否在进行中。 ///交互控制器 private var interactivePopTransition : LSPercentDrivenInteractiveTransition!

在viewDidLoad:中添加

self.delegate = self let gesture = UIScreenEdgePanGestureRecognizer(target:self,action:#selector(handleGesture(gestureRecognizer:))) gesture.edges = .left self.view.addGestureRecognizer

并实现手势的方法:

 // 以下----使用UIPercentDrivenInteractiveTransition交互控制器 func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) { var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width progress = min(1.0, max(0.0, progress)) // let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview) // var progress = Float(translation.x / 200) // progress = fminf(fmaxf(progress, 0.0), 1.0) print("") if gestureRecognizer.state == UIGestureRecognizerState.began { print self.interactivePopTransition = LSPercentDrivenInteractiveTransition() interactionInProgress = true self.popViewController(animated: true) } else if gestureRecognizer.state == UIGestureRecognizerState.changed { self.interactivePopTransition.update(CGFloat) print("Changed") } else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled { if progress > 0.5 { self.interactivePopTransition.finish() print("finished") } else { self.interactivePopTransition.cancel() print("canceled") } interactionInProgress = false self.interactivePopTransition = nil } }
  1. 手势开始后,我们初始化交互控制器self.interactivePopTransition,调整interactionInProgress的值并触发关闭视图控制器的操作。
  1. 手势进行时,我们不断调用update方法更新进度。它是UIPercentDrivenInteractiveTransition的一个方法,根据你传入的百分比值更新过渡动画。
  2. 如果手势被取消,更新interactionInProgress的值,并回滚过渡动画。
  3. 手势完成后,根据当前进度判断是取消还是完成过渡动画。

LSNavigationController中实现UINavigationControllerDelegate协议,

/// UINavigationControllerDelegate 以下两个协议均实现时,以第二个为准, func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == UINavigationControllerOperation.push { return LSPushAnimation.init() }else if operation == UINavigationControllerOperation.pop { return LSPopAnimation.init() } return nil } /// 当返回值为nil时,上面的协议返回的push和pop动画才会有作用 func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { if interactivePopTransition != nil { return interactivePopTransition } return nil }

这里的第一个方法前面已经用过,第二个是返回交互控制器,因为如果交互控制器不为空的话,就会调用控制器来控制交互,这样就使上面的push和pop失去的效果,所以只有在需要自定义交互控制器时才会返回,不然则返回nil即可(就像自定义滑动返回手势)。所以在上面的手势处理中才会在开始时初始化控制器,在结束后制为nil。

使用UIPercentDrivenInteractiveTransition的Demo

自定义交互控制器

在上面的demo基础上修改。

LSPercentDrivenInteractiveTransition需要自己实现UIViewControllerInteractiveTransitioning协议。UIViewControllerInteractiveTransitioning协议总共有三个方法,其中startInteractiveTransition:是必须实现的方法,我们在里面初始化动画的状态:

///以下是自定义交互控制器 ///先初始化需要的变量 var transitionContext : UIViewControllerContextTransitioning! var transitingView : UIView!/// 以下----自定义交互控制器 override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { self.transitionContext = transitionContext let containerView = transitionContext.containerView let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) containerView.insertSubview((toViewController?.view)!, belowSubview: (fromViewController?.view)!) self.transitingView = fromViewController?.view } override func update(_ percentComplete: CGFloat) { let scale = CGFloat(fabsf(Float(percentComplete - CGFloat transitingView?.transform = CGAffineTransform(scaleX: scale, y: scale) transitionContext?.updateInteractiveTransition(percentComplete) } func finishBy(cancelled: Bool) { if cancelled { UIView.animate(withDuration: 0.4, animations: { self.transitingView!.transform = CGAffineTransform(scaleX: 1, y: 1) }, completion: {completed in self.transitionContext!.cancelInteractiveTransition() self.transitionContext!.completeTransition } else { UIView.animate(withDuration: 0.4, animations: { print(self.transitingView) self.transitingView!.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) print(self.transitingView) }, completion: {completed in self.transitionContext!.finishInteractiveTransition() self.transitionContext!.completeTransition } }

update:方法用来更新view的transform属性,finishBy:方法主要用来判断是进入下一个页面还是返回到之前的页面,并告知transitionContext目前的状态,以及对当前正在scale的view做最后的动画。这里的transitionContext和transitingView可以在前面的处理手势识别代码中取得,因此手势的处理中变成了:

///以下是自定义交互器 func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) { var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width progress = min(1.0, max(0.0, progress)) // let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview) // var progress = Float(translation.x / 200) // progress = fminf(fmaxf(progress, 0.0), 1.0) print("") if gestureRecognizer.state == UIGestureRecognizerState.began { print self.interactivePopTransition = LSPercentDrivenInteractiveTransition() interactionInProgress = true self.popViewController(animated: true) } else if gestureRecognizer.state == UIGestureRecognizerState.changed { interactivePopTransition.update print("Changed") } else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled { interactivePopTransition.finishBy(cancelled: progress < 0.5) interactionInProgress = false self.interactivePopTransition = nil } }

这样就完成了自定义交互控制器的全部内容。

自定义控制器的demo

注意,视图控制器的这些同样适用于model视图的动画(连接中包括三个工程,其中一个是图片浏览,swift代码还不太熟悉,需要改善)。

针对这两个问题

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == UINavigationControllerOperation.push { return LSPushAnimation.init() }else if operation == UINavigationControllerOperation.pop { return LSPopAnimation.init() } return nil }

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

为了在基于UINavigationController下做自定义的动画切换,1、先建立一个简单的工程,建一个UINavigationController的子类LSNavigationController,另外两个VC viewControllersecondViewController,注意:viewController是一个UINavigationController。在这两个页面中先做一些准备工作就是各有一个按钮,一个做push操作,一个做pop操作。2、LSNavigationController ,用来实现UINavigationControllerDelegate```协议。在类中实现代理函数

本文由金沙澳门官网网址发布于电脑系统,转载请注明出处:【金沙澳门官网网址】iOS页面控制切换控制,自

关键词: