カスタムのインタラクティブなトランジションでUIView.animateWithDurationのcompletionが呼ばれないことがある？


- dismissのトランジション
- スワイプに連動したトランジション
- iOS 8.3
- もの凄い勢いでスワイプする(笑)

で起きたのでメモ。

# 8.3以前は動いていたコード

```swift
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
  let from: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
  let to: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

  …
  
  UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
    () -> Void in
    
    // ここで色々やる
        
  }) {
    (finished) -> Void in
    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
  }
}
```

という感じで、アニメーションを定義しておいて

- UIScreenEdgePanGestureRecognizer
- UIPanGestureRecognizer

と

- UIPercentDrivenInteractiveTransition

を組み合わせて、スワイプするとdismissするように作る

```swift
switch gestureRecognizer.state {
  case .Began:
    if location.x < 0 { return }
    
    interactiveTransition = UIPercentDrivenInteractiveTransition()
    interactionInProgress = true

    dismissViewControllerAnimated(true, completion: nil)
    
  case .Changed:
    interactiveTransition?.updateInteractiveTransition(progress)
    
  case .Ended, .Cancelled:
    interactionInProgress = false
    if progress > 0.35 {
      interactiveTransition?.finishInteractiveTransition()
    } else {
      interactiveTransition?.cancelInteractiveTransition()
    }
    
  default:
    return
}
```

こんな感じでやっていた。

# 起きた問題

この状態でiOS 8.3でもの凄い勢いでスワイプすると

- `finishInteractiveTransition`
- `cancelInteractiveTransition`

は呼ばれるけど、`animateTransition`内の`animateWithDuration`の`completion`が呼ばれないという現象が起きた。

そのせいで、トランジションが完了したと認識できずに画面がフリーズしたような状態になってしまった。

# 解決策

場当たり的だけど、`finishInteractiveTransition`と`cancelInteractiveTransition`を呼び出すルートに入るのはわかっていたので、ちょっとだけディレイさせてトランジションを強制的に終わらせるようにしたところ改善した。

もっと良い方法がないのだろうか…。

## コード

- アニメーションする所

```swift
private var currentTransitionContext: UIViewControllerContextTransitioning!

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
  currentTransitionContext = transitionContext
  let from: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
  let to: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

  …
  
  UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
    () -> Void in
    
    // ここで色々やる
        
  }) {
    (finished) -> Void in
    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    self.currentTransitionContext = nil
  }
}

func forceFinish() {
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
    if let transitionContext = self.currentTransitionContext {
      transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
      self.currentTransitionContext = nil
    }
  }
}
```

- ジェスチャーの認識

```swift
switch gestureRecognizer.state {
  case .Began:
    if location.x < 0 { return }
    
    interactiveTransition = UIPercentDrivenInteractiveTransition()
    interactionInProgress = true

    dismissViewControllerAnimated(true, completion: nil)
    
  case .Changed:
    interactiveTransition?.updateInteractiveTransition(progress)
    
  case .Ended, .Cancelled:
    interactionInProgress = false
    if progress > 0.35 {
      interactiveTransition?.finishInteractiveTransition()
    } else {
      interactiveTransition?.cancelInteractiveTransition()
    }
    
    forceFinish()
    
  default:
    return
}
```