Upgrade to Pro — share decks privately, control downloads, hide ads and more …

触り心地の良い Interactive Transitionをマスターしよう - iOSDC 2017

Shunki Tan
September 17, 2017

触り心地の良い Interactive Transitionをマスターしよう - iOSDC 2017

日本のアプリは海外と比べて、触っていて気持ちの良い遷移アニメーションが少ないなと感じています。Facebookのように写真プレビューをPanジェスチャーで閉じるケースを題材に、触り心地の良いInteractive Transitionの実装方法をご紹介します。

Shunki Tan

September 17, 2017
Tweet

More Decks by Shunki Tan

Other Decks in Programming

Transcript

  1. let controller = ImagePreviewViewController() // ը૾ϓϨϏϡʔը໘ͷViewController controller.transitioningDelegate = ImagePreviewTransition() present(controller,

    animated: true, completion: nil) TransitionDelegate Transitionͷͱ͖ʹͲΜͳભҠΞχϝʔγϣϯΛߦ͏͔Λܾఆ͢Δ
  2. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  3. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  4. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  5. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  6. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  7. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  8. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  9. public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol { @available(iOS 2.0, *) optional

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? @available(iOS 2.0, *) optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? @available(iOS 8.0, *) optional public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? }
  10. public protocol UIViewControllerInteractiveTransitioning : NSObjectProtocol { public func startInteractiveTransition(_ transitionContext:

    UIViewControllerContextTransitioning) optional public var completionSpeed: CGFloat { get } optional public var completionCurve: UIViewAnimationCurve { get } /// In 10.0, if an object conforming to UIViewControllerAnimatedTransitioning is /// known to be interruptible, it is possible to start it as if it was not /// interactive and then interrupt the transition and interact with it. In this /// case, implement this method and return NO. If an interactor does not /// implement this method, YES is assumed. @available(iOS 10.0, *) optional public var wantsInteractiveStart: Bool { get } }
  11. public protocol UIViewControllerInteractiveTransitioning : NSObjectProtocol { public func startInteractiveTransition(_ transitionContext:

    UIViewControllerContextTransitioning) optional public var completionSpeed: CGFloat { get } optional public var completionCurve: UIViewAnimationCurve { get } /// In 10.0, if an object conforming to UIViewControllerAnimatedTransitioning is /// known to be interruptible, it is possible to start it as if it was not /// interactive and then interrupt the transition and interact with it. In this /// case, implement this method and return NO. If an interactor does not /// implement this method, YES is assumed. @available(iOS 10.0, *) optional public var wantsInteractiveStart: Bool { get } }
  12. public protocol UIViewControllerInteractiveTransitioning : NSObjectProtocol { public func startInteractiveTransition(_ transitionContext:

    UIViewControllerContextTransitioning) optional public var completionSpeed: CGFloat { get } optional public var completionCurve: UIViewAnimationCurve { get } /// In 10.0, if an object conforming to UIViewControllerAnimatedTransitioning is /// known to be interruptible, it is possible to start it as if it was not /// interactive and then interrupt the transition and interact with it. In this /// case, implement this method and return NO. If an interactor does not /// implement this method, YES is assumed. @available(iOS 10.0, *) optional public var wantsInteractiveStart: Bool { get } }
  13. Gesture: .began func handleGesture(_ gesture: UIPanGestureRecognizer) { let translation =

    gesture.translation(in: gesture.view) let velocity = gesture.velocity(in: gesture.view) var progress = abs(translation.y) / 200 progress = CGFloat(min(max(progress, 0.0), 1.0)) switch gesture.state { case .possible: break case .began: interactionInProgress = true previewViewController.dismiss(animated: true, completion: nil) case .changed: updateInteraction(progress: progress, translation: translation) case .cancelled, .failed: interactionInProgress = false completeInteraction(velocity: velocity, finished: false) case .ended: interactionInProgress = false let finished = progress > 0.3 || (progress > 0.1 && abs(velocity.y) > 100) completeInteraction(velocity: velocity, finished: finished) } }
  14. Gesture: .began func handleGesture(_ gesture: UIPanGestureRecognizer) { let translation =

    gesture.translation(in: gesture.view) let velocity = gesture.velocity(in: gesture.view) var progress = abs(translation.y) / 200 progress = CGFloat(min(max(progress, 0.0), 1.0)) switch gesture.state { case .possible: break case .began: interactionInProgress = true previewViewController.dismiss(animated: true, completion: nil) case .changed: updateInteraction(progress: progress, translation: translation) case .cancelled, .failed: interactionInProgress = false completeInteraction(velocity: velocity, finished: false) case .ended: interactionInProgress = false let finished = progress > 0.3 || (progress > 0.1 && abs(velocity.y) > 100) completeInteraction(velocity: velocity, finished: finished) } } Dismiss ViewController
  15. Gesture: .began func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { isPresenting

    = false return interactionInProgress ? self : nil } Dismiss ViewController TransitionDelegate nilҎ֎Λฦ͢ͱɺInteractiveTransition͕࢝·Δ
  16. func handleGesture(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: gesture.view)

    let velocity = gesture.velocity(in: gesture.view) var progress = abs(translation.y) / 200 progress = CGFloat(min(max(progress, 0.0), 1.0)) switch gesture.state { case .possible: break case .began: interactionInProgress = true destinationViewController.dismiss(animated: true, completion: nil) case .changed: updateInteraction(progress: progress, translation: translation) case .cancelled, .failed: interactionInProgress = false completeInteraction(velocity: velocity, finished: false) case .ended: interactionInProgress = false let finished = progress > 0.3 || (progress > 0.1 && abs(velocity.y) > 100) completeInteraction(velocity: velocity, finished: finished) } } Gesture: .began Dismiss ViewController TransitionDelegate InteractiveTransitioning Gesture: .changed
  17. UIView.animate(withDuration: 0.1, delay: 0, options: [.allowUserInteraction, .beginFromCurrentState, .curveLinear], animations: {

    dimmingView.alpha = 1.0 - (0.7 * progress) var center = destinationImageFrame.center center.x += translation.x center.y += translation.y snapshotView.center = center let scale = 1.0 - (0.2 * progress) snapshotView.transform = CGAffineTransform(scaleX: scale, y: scale) }, completion: nil) Duration = 0.1ʹ͢Δͱ଎͘δΣενϟʔΛ͠ ͨͱ͖ʹࣗવʹͳΔ Gesture: .began Dismiss ViewController TransitionDelegate InteractiveTransitioning Gesture: .changed
  18. UIView.animate(withDuration: 0.1, delay: 0, options: [.allowUserInteraction, .beginFromCurrentState, .curveLinear], animations: {

    dimmingView.alpha = 1.0 - (0.7 * progress) var center = destinationImageFrame.center center.x += translation.x center.y += translation.y snapshotView.center = center let scale = 1.0 - (0.2 * progress) snapshotView.transform = CGAffineTransform(scaleX: scale, y: scale) }, completion: nil) Easing Curve͸Linearʹ͢Δ Gesture: .began Dismiss ViewController TransitionDelegate InteractiveTransitioning Gesture: .changed
  19. func handleGesture(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: gesture.view)

    let velocity = gesture.velocity(in: gesture.view) var progress = abs(translation.y) / 200 progress = CGFloat(min(max(progress, 0.0), 1.0)) switch gesture.state { case .possible: break case .began: interactionInProgress = true destinationViewController.dismiss(animated: true, completion: nil) case .changed: updateInteraction(progress: progress, translation: translation) case .cancelled, .failed: interactionInProgress = false completeInteraction(velocity: velocity, finished: false) case .ended: interactionInProgress = false let finished = progress > 0.3 || (progress > 0.1 && abs(velocity.y) > 100) completeInteraction(velocity: velocity, finished: finished) } } Gesture: .ended Gesture: .began Dismiss ViewController TransitionDelegate InteractiveTransitioning Gesture: .changed
  20. Gesture: .ended UIView.animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat,

    initialSpringVelocity velocity: CGFloat, options: UIViewAnimationOptions = [], animations: @escaping () -> Swift.Void, completion: ((Bool) -> Swift.Void)? = nil )
  21. UIView.animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity:

    CGFloat, options: UIViewAnimationOptions = [], animations: @escaping () -> Swift.Void, completion: ((Bool) -> Swift.Void)? = nil ) ݫີʹ΍ΔͳΒɺݮਰৼಈͷӡಈํఔࣜΛղ͘ඞཁ͕͋Δ Gesture: .ended
  22. func handleGesture(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: gesture.view)

    let velocity = gesture.velocity(in: gesture.view) var progress = abs(translation.y) / 200 progress = CGFloat(min(max(progress, 0.0), 1.0)) switch gesture.state { case .possible: break case .began: interactionInProgress = true destinationViewController.dismiss(animated: true, completion: nil) case .changed: updateInteraction(progress: progress, translation: translation) case .cancelled, .failed: interactionInProgress = false completeInteraction(velocity: velocity, finished: false) case .ended: interactionInProgress = false let finished = progress > 0.3 || (progress > 0.1 && abs(velocity.y) > 100) completeInteraction(velocity: velocity, finished: finished) } } Gesture: .ended
  23. // ΞχϝʔγϣϯΛࣗવʹݟͤΔͨΊʹɺݮਰͷΞχϝʔγϣϯ͕ඞཁ POPDecayAnimation(propertyNamed: kPOPViewCenter).apply { // PanGestureͷvelocity $0.velocity = velocity

    $0.deceleration = 0.9 $0.completionBlock = { _ in // ݮਰͯ͠WFMPDJUZʹͳͬͨΒɺ // ImageViewͷframe, scaleͷSpringΞχϝʔγϣϯΛ։࢝͢Δ snapshotView.pop_add(frameAnimation, forKey: "snapshot.frame") snapshotView.pop_add(scaleAnimation, forKey: "snapshot.transform") } snapshotView.pop_add($0, forKey: "snapshot.center") } Gesture: .ended 1. ݮਰͷΞχϝʔγϣϯ
  24. // ΞχϝʔγϣϯΛࣗવʹݟͤΔͨΊʹɺݮਰͷΞχϝʔγϣϯ͕ඞཁ POPDecayAnimation(propertyNamed: kPOPViewCenter).apply { // PanGestureͷvelocity $0.velocity = velocity

    $0.deceleration = 0.9 $0.completionBlock = { _ in // ݮਰͯ͠WFMPDJUZʹͳͬͨΒɺ // ImageViewͷframe, scaleͷSpringΞχϝʔγϣϯΛ։࢝͢Δ snapshotView.pop_add(frameAnimation, forKey: "snapshot.frame") snapshotView.pop_add(scaleAnimation, forKey: "snapshot.transform") } snapshotView.pop_add($0, forKey: "snapshot.center") } Gesture: .ended 1. ݮਰͷΞχϝʔγϣϯ
  25. // ΞχϝʔγϣϯΛࣗવʹݟͤΔͨΊʹɺݮਰͷΞχϝʔγϣϯ͕ඞཁ POPDecayAnimation(propertyNamed: kPOPViewCenter).apply { // PanGestureͷvelocity $0.velocity = velocity

    $0.deceleration = 0.9 $0.completionBlock = { _ in // ݮਰͯ͠WFMPDJUZʹͳͬͨΒɺ // ImageViewͷframe, scaleͷSpringΞχϝʔγϣϯΛ։࢝͢Δ snapshotView.pop_add(frameAnimation, forKey: "snapshot.frame") snapshotView.pop_add(scaleAnimation, forKey: "snapshot.transform") } snapshotView.pop_add($0, forKey: "snapshot.center") } Gesture: .ended 1. ݮਰͷΞχϝʔγϣϯ
  26. let group = DispatchGroup() POPBasicAnimation.easeInOut(propertyNamed: kPOPViewAlpha).apply { group.enter() $0.duration =

    duration $0.toValue = finished ? 0.0 : 1.0 $0.completionBlock = { _ in group.leave() } dimmingView.pop_add($0, forKey: "dimming.alpha") } let frameAnimation = POPSpringAnimation(propertyNamed: kPOPViewFrame).apply { group.enter() $0.toValue = finished ? NSValue(cgRect: self.sourceImageFrame!) : NSValue(cgRect: self.destinationImageFrame!) $0.springBounciness = 3 $0.springSpeed = 3 $0.completionBlock = { _ in group.leave() } } let scaleAnimation = POPSpringAnimation(propertyNamed: kPOPViewScaleXY).apply { group.enter() $0.toValue = NSValue(cgSize: CGSize(square: 1)) $0.springBounciness = 3 $0.springSpeed = 3 $0.completionBlock = { _ in group.leave() } } Gesture: .ended 2. SpringΞχϝʔγϣϯ
  27. let group = DispatchGroup() POPBasicAnimation.easeInOut(propertyNamed: kPOPViewAlpha).apply { group.enter() $0.duration =

    duration $0.toValue = finished ? 0.0 : 1.0 $0.completionBlock = { _ in group.leave() } dimmingView.pop_add($0, forKey: "dimming.alpha") } let frameAnimation = POPSpringAnimation(propertyNamed: kPOPViewFrame).apply { group.enter() $0.toValue = finished ? NSValue(cgRect: self.sourceImageFrame!) : NSValue(cgRect: self.destinationImageFrame!) $0.springBounciness = 3 $0.springSpeed = 3 $0.completionBlock = { _ in group.leave() } } let scaleAnimation = POPSpringAnimation(propertyNamed: kPOPViewScaleXY).apply { group.enter() $0.toValue = NSValue(cgSize: CGSize(square: 1)) $0.springBounciness = 3 $0.springSpeed = 3 $0.completionBlock = { _ in group.leave() } } Gesture: .ended 3. શͯͷΞχϝʔγϣϯ͕׬ྃͨ͠Β…
  28. group.notify(queue: .main) { if finished { self.previewSource?.sourceImageView?.isHidden = false UIView.animate(withDuration:

    duration * 0.25, delay: 0, options: [.curveEaseInOut], animations: { snapshotView.alpha = 0 }, completion: { _ in destination.view.removeFromSuperview() dimmingView.removeFromSuperview() snapshotView.removeFromSuperview() transitionContext.finishInteractiveTransition() transitionContext.completeTransition(true) }) } else { // TransitionΛΩϟϯηϧͨ͠ͱ͖ } } Gesture: .ended 3. શͯͷΞχϝʔγϣϯ͕׬ྃͨ͠Β…
  29. group.notify(queue: .main) { if finished { self.previewSource?.sourceImageView?.isHidden = false UIView.animate(withDuration:

    duration * 0.25, delay: 0, options: [.curveEaseInOut], animations: { snapshotView.alpha = 0 }, completion: { _ in destination.view.removeFromSuperview() dimmingView.removeFromSuperview() snapshotView.removeFromSuperview() transitionContext.finishInteractiveTransition() transitionContext.completeTransition(true) }) } else { // TransitionΛΩϟϯηϧͨ͠ͱ͖ } } Gesture: .ended 4.TransitionΛऴྃ͢Δ