モーダル遷移のアニメーションをカスタムする

## はじめに
アプリでモーダル遷移するときにデフォルトのアニメーションがいくつか用意されていますが、それ以外のアニメーションを使いたいときは、自分でカスタムすることができるようです。
今回は、`UIViewControllerAnimatedTransitioning`を使って、横スワイプしたときのようなアニメーションを実装してみました。

## 実装イメージ
![カスタムアニメーション.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580445/003d1603-f731-f0f2-6f6c-0a612ce49c6b.gif)


## コード

#### モーダルを呼び出す側のコード
```swift:BaseViewController.swift
import UIKit

class BaseViewController: UIViewController {
    
    @IBOutlet weak var buttonShow: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    @IBAction func showModalView(_ sender: Any) {
        guard let modalVC = UIStoryboard(name: String(describing: CustomModalViewController.self), bundle: nil)
                .instantiateInitialViewController() as? CustomModalViewController else { return }
        
        modalVC.modalPresentationStyle = .fullScreen
        present(modalVC, animated: true)
    }
}
```

#### モーダルとして呼び出される側のコード
```swift:CustomModalViewController.swift
import UIKit

class CustomModalViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        transitioningDelegate = self
    }
    
    @IBAction func touchCloseButton(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
}

// モーダルとして呼び出す側に「UIViewControllerTransitioningDelegate」を継承させる。
// MARK: UIViewControllerTransitioningDelegate
extension CustomModalViewController: UIViewControllerTransitioningDelegate {
    
    // presentのときに呼ばれる
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimater(animationMode: .present)
    }

    // dismissのときに呼ばれる
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimater(animationMode: .dismiss)
    }
}
```

#### アニメーションを定義するクラス
```swift:CustomAnimeter.swift
class CustomAnimater: NSObject {
    
    // 開くと閉じるしかないので、enumで定義しておくと楽
    enum AnimationMode {
        case present, dismiss
    }
    private let animationMode: AnimationMode
    private let duration = 0.3
    
    init(animationMode: AnimationMode) {
        self.animationMode = animationMode
    }
}

extension CustomAnimater: UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if animationMode == .present {
            guard let toView = transitionContext.view(forKey: .to) else { return }
            guard let fromView = transitionContext.view(forKey: .from) else { return }
            transitionContext.containerView.addSubview(toView)
            toView.alpha = 0.0
            toView.transform = CGAffineTransform(translationX:fromView.bounds.size.width * 0.8 , y: 0)
            UIView.animate(withDuration: 0.5, delay: 0) { [weak self] in
                guard let _ = self else { return }
                toView.transform = CGAffineTransform(translationX: 0, y: 0)
                toView.alpha = 1.0
            } completion: { finish in
                toView.transform = .identity
                transitionContext.completeTransition(finish)
            }
        } else {
            guard let toView = transitionContext.view(forKey: .to) else { return }
            guard let fromView = transitionContext.view(forKey: .from) else { return }
            transitionContext.containerView.addSubview(toView)
            transitionContext.containerView.addSubview(fromView)
            fromView.alpha = 1
            UIView.animate(withDuration: 0.5, delay: 0) {
                fromView.alpha = 0.0
                fromView.transform = CGAffineTransform(translationX:-fromView.bounds.size.width * 0.8 , y: 0)
            } completion: { finish in
                // transformをもとに戻さないとバグの要因になることがあるみたい
                fromView.transform = .identity
                transitionContext.completeTransition(finish)
            }
        }
    }
}
```