Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a working interruptible view controller transition on iOS?

iOS 10 added a new function for custom animated view controller transitions called interruptibleAnimator(using:)

Lots of people appear to be using the new function, however by simply implementing their old animateTransition(using:) within the animation block of a UIViewPropertyAnimator in interruptibleAnimator(using:) (see Session 216 from 2016)

However I can't find a single example of someone actually using the interruptible animator for creating interruptible transitions. Everyone seems to support it, but no one actually uses it.

For example, I created a custom transition between two UIViewControllers using a UIPanGestureRecognizer. Both view controllers have a backgroundColor set, and a UIButton in the middle that changes the backgroundColour on touchUpInside.

Now I've implemented the animation simply as:

  1. Setup the toViewController.view to be positioned to the left/right (depending on the direction needed) of the fromViewController.view

  2. In the UIViewPropertyAnimator animation block, I slide the toViewController.view into view, and the fromViewController.view out of view (off screen).

Now, during transition, I want to be able to press that UIButton. However, the button press was not called. Odd, this is how the session implied things should work, I setup a custom UIView to be the view of both of my UIViewControllers as follows:

class HitTestView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        if view is UIButton {
            print("hit button, point: \(point)")
        }
        return view
    }
}

class ViewController: UIViewController {

     let button = UIButton(type: .custom)

     override func loadView() {
         self.view = HitTestView(frame: UIScreen.main.bounds)
     }
    <...>
}

and logged out the func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? results. The UIButton is being hitTested, however, the buttons action is not called.

Has anyone gotten this working?

Am I thinking about this wrong and are interruptible transitions just to pausing/resuming a transition animation, and not for interaction?

Almost all of iOS11 uses what I believe are interruptible transitions, allowing you to, for example, pull up control centre 50% of the way and interact with it without releasing the control centre pane then sliding it back down. This is exactly what I wish to do.

Thanks in advance! Spent way to long this summer trying to get this working, or finding someone else trying to do the same.

like image 340
Stephen Heaps Avatar asked Oct 15 '25 10:10

Stephen Heaps


2 Answers

Here you go! A short example of an interruptible transition. Add your own animations in the addAnimation block to get things going.

 class ViewController: UIViewController {
  var dismissAnimation: DismissalObject?
  override func viewDidLoad() {
    super.viewDidLoad()
    self.modalPresentationStyle = .custom
    self.transitioningDelegate = self
    dismissAnimation = DismissalObject(viewController: self)
  }
}

extension ViewController: UIViewControllerTransitioningDelegate {
  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return dismissAnimation
  }

  func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    guard let animator = animator as? DismissalObject else { return nil }
    return animator
  }
}

class DismissalObject: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning {
  fileprivate var shouldCompleteTransition = false
  var panGestureRecongnizer: UIPanGestureRecognizer!
  weak var viewController: UIViewController!
  fileprivate var propertyAnimator: UIViewPropertyAnimator?
  var startProgress: CGFloat = 0.0

  var initiallyInteractive = false
  var wantsInteractiveStart: Bool {
    return initiallyInteractive
  }

  init(viewController: UIViewController) {
    self.viewController = viewController
    super.init()
    panGestureRecongnizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
    viewController.view.addGestureRecognizer(panGestureRecongnizer)
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 8.0 // slow animation for debugging
  }

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}

  func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
    let animator = interruptibleAnimator(using: transitionContext)
    if transitionContext.isInteractive {
        animator.pauseAnimation()
    } else {
        animator.startAnimation()
    }
  }

  func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
    // as per documentation, we need to return existing animator
    // for ongoing transition

    if let propertyAnimator = propertyAnimator {
        return propertyAnimator
    }

    guard let fromVC = transitionContext.viewController(forKey: .from),
        let toVC = transitionContext.viewController(forKey: .to)
        else { fatalError("fromVC or toVC not found") }

    let containerView = transitionContext.containerView

    // Do prep work for animations

    let duration = transitionDuration(using: transitionContext)
    let timingParameters = UICubicTimingParameters(animationCurve: .easeOut)
    let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timingParameters)
    animator.addAnimations {
        // animations
    }

    animator.addCompletion { [weak self] (position) in
        let didComplete = position == .end
        if !didComplete {
            // transition was cancelled
        }

        transitionContext.completeTransition(didComplete)

        self?.startProgress = 0
        self?.propertyAnimator = nil
        self?.initiallyInteractive = false
    }

    self.propertyAnimator = animator
    return animator
  }

  @objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
    switch gestureRecognizer.state {
    case .began:
        initiallyInteractive = true
        if !viewController.isBeingDismissed {
            viewController.dismiss(animated: true, completion: nil)
        } else {
            propertyAnimator?.pauseAnimation()
            propertyAnimator?.isReversed = false
            startProgress = propertyAnimator?.fractionComplete ?? 0.0
        }
        break
    case .changed:
        let translation = gestureRecognizer.translation(in: nil)

        var progress: CGFloat = translation.y / UIScreen.main.bounds.height
        progress = CGFloat(fminf(fmaxf(Float(progress), -1.0), 1.0))

        let velocity = gestureRecognizer.velocity(in: nil)
        shouldCompleteTransition = progress > 0.3 || velocity.y > 450

        propertyAnimator?.fractionComplete = progress + startProgress
        break
    case .ended:
        if shouldCompleteTransition {
            propertyAnimator?.startAnimation()
        } else {
            propertyAnimator?.isReversed = true
            propertyAnimator?.startAnimation()
        }
        break
    case .cancelled:
        propertyAnimator?.isReversed = true
        propertyAnimator?.startAnimation()
        break
    default:
        break
    }
  }
}
like image 175
Woody Jean-louis Avatar answered Oct 18 '25 01:10

Woody Jean-louis


I have published sample code and a reusable framework that demonstrates interruptible view controller animation transitions. It's called PullTransition and it makes it easy to either dismiss or pop a view controller simply by swiping downward. Please let me know if the documentation needs improvement. I hope this helps!

like image 20
aleksey Avatar answered Oct 18 '25 01:10

aleksey