UIViewControllerAnimatedTransitioning によるカスタムトランジション

UINavigationController とモーダルの場合とで実装方法が異なるのでメモ。

# UINavigationController のトランジション（プッシュ／ポップ遷移）をカスタマイズ

方針：

- UIViewControllerAnimatedTransitioning に準拠したアニメーターオブジェクトを実装
- UINavigationControllerDelegate を実装してアニメーターオブジェクトを返却
- `pushViewController(_:animated:)` または `popViewControllerAnimated(_:)` を実施


## アニメーターオブジェクト

UIViewControllerAnimatedTransitioning に準拠したアニメーターを実装する。一般的には以下のように実装する。

```swift
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
	
	func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
		return 0.35
	}
	
	func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
		// 遷移元ビューコントローラー
		let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
		// 遷移先ビューコントローラー
		let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
		// トランジションコンテクストからアニメーションを描画するためのコンテナービューを取得
		let containerView = transitionContext.containerView
		
		// トランジションコンテクストのコンテナービューに to.view を乗せる
		containerView.insertSubview(to.view, belowSubview: from.view)
		
		// コンテナービュー上でアニメーションを描画。transition でも良い
		UIView.animate(withDuration: transitionDuration(using: transitionContext),
		               delay: 0,
		               options: UIViewAnimationOptions.init(rawValue: 0),
		               animations: {
						from.view.alpha = 0.0
		},
		               completion: { (finished: Bool) in
						from.view.alpha = 1.0
						// トランジションが完了したことを通知
						transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
		})
		
	}

}
```

これを更に Push / Pop を考慮するように修正。

```swift
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
	
	var navigationOperation: UINavigationControllerOperation = .none
	
	convenience init(_ navigationOperation: UINavigationControllerOperation) {
		self.init()
		self.navigationOperation = navigationOperation
	}
	
	func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
		return 0.35
	}
	
	func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
		customAnimation(transitionContext)
	}
	
	private func customAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
		let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
		let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
		let containerView = transitionContext.containerView
		
		containerView.insertSubview(to.view, belowSubview: from.view)
		
		UIView.animate(withDuration: transitionDuration(using: transitionContext),
		               delay: 0,
		               options: UIViewAnimationOptions.init(rawValue: 0),
		               animations: {
						from.view.alpha = 0.0
		},
		               completion: { _ in
						from.view.alpha = 1.0
						transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
		})
	}

}
```

## アニメーションに UIView.transition() を利用する場合

例えば `customAnimation()` の中身をこのようにすれば良い。以下の例では左右にフリップする。

```swift
private func customAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
	let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
	let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
	let containerView = transitionContext.containerView
	let options: UIViewAnimationOptions = (navigationOperation == UINavigationControllerOperation.pop) ? [.transitionFlipFromLeft] : [.transitionFlipFromRight]
	
	containerView.insertSubview(to.view, belowSubview: from.view)
		
	UIView.transition(from: from.view,
	                  to: to.view,
	                  duration: transitionDuration(using: transitionContext),
	                  options: options,
	                  completion: { _ in
							transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
	})
}
```

## UINavigationControllerDelegate 

UINavigationControllerDelegate に準拠したアニメーションコントローラーを実装する。ここで適当なアニメーターを返す。

```swift:UINavigationControllerDelegate
// Push: From -> To / Pop: From <- To それぞれのペアを定義する
private var classPairs: [(from: AnyClass, to: AnyClass)] {
	return [
		(from: MasterViewController.self, to: DetailViewController.self),
		(from: DetailViewController.self, to: MasterViewController.self),
	]
}

private func isTargetViewControllerPair(from: UIViewController, to: UIViewController) -> Bool {
	for pair in classPairs {
		if from.classForCoder == pair.from && to.classForCoder == pair.to {
			return true
		}
	}
	
	return false
}


func navigationController(_ navigationController: UINavigationController,
                          animationControllerFor operation: UINavigationControllerOperation,
                          from fromVC: UIViewController,
                          to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
	if isTargetViewControllerPair(from: fromVC, to: toVC) {
		// 遷移元/先が対象ビューコントローラーであれば、適当なアニメーターを返す
		return Animator(operation)
	}
	
	return nil
}

```

Storyboard の UINavigationController にアニメーションコントローラーのインスタンスを配置して、デリゲートの接続を行えば良い。

![](https://qiita-image-store.s3.amazonaws.com/0/3603/0d8e7179-6baa-104f-f6f6-a1e8d77ab894.png)

## 遷移処理を実施

`pushViewController(_:animated:)` または `popViewControllerAnimated(_:)` を実施するとアニメーターで実装したアニメーションが適用される。Storyboard Segue による遷移でも同様のはず。


# モーダルビューのトランジションをカスタマイズ

UIViewControllerTransitioningDelegate でアニメーターを返すデリゲートメソッドを実装する。

```swift

extension HogeViewController: UIViewControllerTransitioningDelegate {
	
	func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
		return Animator()
	}
	
	func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
		return Animator()
	}
	
	func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
		return InteractiveAnimator() // UIViewControllerInteractiveTransitioning
	}
	
	func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
		return InteractiveAnimator() // UIViewControllerInteractiveTransitioning
	}
	
}
```