38
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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

ファイル構成

  • ViewController
  • ModalNavigationController
  • ModalViewController
  • DismissAnimator
  • Interactor

コード

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
	}
}
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
        }
    }
}
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)
	}
}
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)
            }
        )
    }
}
Interactor.swift
import UIKit

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

GitHub

38
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?