당겨서 닫을 수있는 모달 구현 (UINavigationController의 경우)

16255 단어 XcodeUIKitSwift


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

    좋은 웹페이지 즐겨찾기