3
votes

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.

2
I myself am struggling with the same problem. It's a shame that there is no proper documentation on this what so ever. Have you figured anything out by now?Christoph

2 Answers

2
votes

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!

1
votes

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
    }
  }
}