引っ張って閉じることができるモーダルを実装する (UINavigationControllerの場合)

<img src="https://qiita-image-store.s3.amazonaws.com/0/15144/5ed87dd0-745d-f4cf-ac92-a23548b2f6a2.gif" width="200">

UIKitでは、ViewController→ViewControllerの遷移をカスタムできます。
上の画像のようにボタンを押すと下から出てきて 引っ張ると閉じることができるモーダルを実装してみます。

https://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

当初こちらのチュートリアルを参考に進めていたのですが、ある問題が発生しました。
それはUINavigationControllerにはPan Gesture Recognizerをアタッチすることができないことです。
そこで、UINavigationControllerの子のViewControllerでPan Gestureをハンドリングし、UINavigationControllerに伝播する方法で目的を達成できましたのでコード片を残しておきたいと思います。

## ファイル構成

* ViewController
* ModalNavigationController
* ModalViewController
* DismissAnimator
* Interactor

## コード

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
	let interactor = Interactor()

	@IBAction func handleButton(_ sender: UIButton) {
		let sb = UIStoryboard(name: "ModalViewController", bundle: nil)
		let nc = sb.instantiateInitialViewController() as! ModalNavigationController
		nc.interactor = interactor
		nc.transitioningDelegate = self
		present(nc, animated: true, completion: nil)
	}

	func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
		return DismissAnimator()
	}

	func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
	}
}
```

```swift:ModalNavigationController.swift
import UIKit

class ModalNavigationController: UINavigationController {
	var interactor: Interactor!

	func handleGesture(_ sender: UIPanGestureRecognizer) {
        let percentThreshold: CGFloat = 0.3

        let translation = sender.translation(in: view)
        let verticalMovement = translation.y / view.bounds.height
        let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
        let downwardMovementPercent = fminf(downwardMovement, 1.0)
        let progress = CGFloat(downwardMovementPercent)
        
        switch sender.state {
        case .began:
            interactor.hasStarted = true
            dismiss(animated: true, completion: nil)
        case .changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.update(progress)
        case .cancelled:
            interactor.hasStarted = false
            interactor.cancel()
        case .ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finish()
                : interactor.cancel()
        default:
            break
        }
    }
}
```

```swift:ModalViewController.swift
import UIKit

class ModalViewController: UIViewController {
	@IBAction func handleDismissButton(_ sender: UIBarButtonItem) {
		dismiss(animated: true, completion: nil)
	}

	@IBAction func handleGesture(_ sender: UIPanGestureRecognizer) {
		weak var nc = navigationController as? ModalNavigationController
		nc?.handleGesture(sender)
	}
}
```

```swift:DismissAnimator.swift
import UIKit

class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1.0
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewController(forKey: .from),
            let toVC = transitionContext.viewController(forKey: .to)
        else { return }

        let containerView = transitionContext.containerView
        
        containerView.insertSubview(toVC.view, belowSubview: fromVC.view)

        let screenBounds = UIScreen.main.bounds
        let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
        let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
        
		UIView.animate(
			withDuration: transitionDuration(using: transitionContext),
            animations: {
                fromVC.view.frame = finalFrame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        )
    }
}
```

```swift:Interactor.swift
import UIKit

class Interactor: UIPercentDrivenInteractiveTransition {
	var hasStarted = false
	var shouldFinish = false
}
```

## GitHub

https://github.com/keisei1092/ModalNavigationControllerPractice