iOS에서 아래로 드래그하여 모달을 해제하는 방법은 무엇입니까?


91

모달을 해제하는 일반적인 방법은 아래로 스 와이프하는 것입니다. 사용자가 모달을 아래로 드래그 할 수 있습니다. 충분히 멀리 있으면 모달이 해제되고 그렇지 않으면 원래 위치로 다시 애니메이션됩니다.

예를 들어 트위터 앱의 사진보기 또는 Snapchat의 "검색"모드에서이 기능을 사용할 수 있습니다.

유사한 스레드는 UISwipeGestureRecognizer 및 [self dismissViewControllerAnimated ...]를 사용하여 사용자가 아래로 스 와이프 할 때 모달 VC를 해제 할 수 있다고 지적합니다. 그러나 이것은 한 번의 스 와이프 만 처리하므로 사용자가 모달을 드래그 할 수 없습니다.


사용자 지정 대화 형 전환을 살펴보십시오. 이것이 구현할 수있는 방법입니다. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX 2015 년

Robert Chen 이 github.com/ThornTechPublic/InteractiveModal repo를 참조하고 모든 것을 처리하기 위해 래퍼 / 핸들러 클래스를 작성했습니다. 더 이상 상용구 코드가 제스처를 해제
Chamira Fernando

@ChamiraFernando, 귀하의 코드를 보았고 많은 도움이되었습니다. 하나가 아닌 여러 방향이 포함되도록 만드는 방법이 있습니까?
Jevon Cowell

내가 할거야. 시간 :( 요즘 거대한 구속이다
Chamira 페르난도

답변:


93

방금 모달을 대화식으로 끌어서 닫는 튜토리얼을 만들었습니다.

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

처음에는이 주제가 혼란스러워서 튜토리얼에서 단계별로 작성합니다.

여기에 이미지 설명 입력

코드를 직접 실행하려면 다음과 같은 저장소가 있습니다.

https://github.com/ThornTechPublic/InteractiveModal

이것이 내가 사용한 접근 방식입니다.

컨트롤러보기

사용자 지정 애니메이션으로 닫기 애니메이션을 재정의합니다. 사용자가 모달을 드래그하면 interactor시작됩니다.

import UIKit

class ViewController: UIViewController {
    let interactor = Interactor()
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let destinationViewController = segue.destinationViewController as? ModalViewController {
            destinationViewController.transitioningDelegate = self
            destinationViewController.interactor = interactor
        }
    }
}

extension ViewController: UIViewControllerTransitioningDelegate {
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissAnimator()
    }
    func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
}

애니메이터 닫기

사용자 지정 애니메이터를 만듭니다. 이것은 UIViewControllerAnimatedTransitioning프로토콜 내부에 패키징하는 커스텀 애니메이션입니다 .

import UIKit

class DismissAnimator : NSObject {
}

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.6
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
            let containerView = transitionContext.containerView()
            else {
                return
        }
        containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
        let screenBounds = UIScreen.mainScreen().bounds
        let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
        let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)

        UIView.animateWithDuration(
            transitionDuration(transitionContext),
            animations: {
                fromVC.view.frame = finalFrame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
            }
        )
    }
}

인터랙 터

UIPercentDrivenInteractiveTransition상태 머신으로 작동 할 수 있도록 하위 클래스를 만듭니다. 두 VC가 상호 작용기 개체에 액세스하므로이를 사용하여 패닝 진행률을 추적하십시오.

import UIKit

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

모달 뷰 컨트롤러

이것은 팬 제스처 상태를 상호 작용기 메서드 호출에 매핑합니다. 이 translationInView() y값은 사용자가 임계 값을 초과했는지 여부를 결정합니다. 이동 제스처가 .Ended이면 인터랙 터가 완료되거나 취소됩니다.

import UIKit

class ModalViewController: UIViewController {

    var interactor:Interactor? = nil

    @IBAction func close(sender: UIButton) {
        dismissViewControllerAnimated(true, completion: nil)
    }

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

        // convert y-position to downward pull progress (percentage)
        let translation = sender.translationInView(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)
        guard let interactor = interactor else { return }

        switch sender.state {
        case .Began:
            interactor.hasStarted = true
            dismissViewControllerAnimated(true, completion: nil)
        case .Changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.updateInteractiveTransition(progress)
        case .Cancelled:
            interactor.hasStarted = false
            interactor.cancelInteractiveTransition()
        case .Ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finishInteractiveTransition()
                : interactor.cancelInteractiveTransition()
        default:
            break
        }
    }

}

4
안녕 로버트, 훌륭합니다. 테이블 뷰와 함께 작동하도록이를 수정하는 방법을 알고 계십니까? 즉, 테이블 뷰가 맨 위에있을 때 풀다운하여 닫을 수 있습니까? 감사합니다
로스 Barbish

1
Ross, 저는 github.com/ThornTechPublic/InteractiveModal/tree/Ross와 같은 작업 예제가있는 새 브랜치를 만들었습니다 . 먼저 어떻게 보이는지 확인하려면 다음 GIF를 확인하십시오. raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . 테이블 뷰에는 target-action을 통해 기존 handleGesture (_ :) 메서드에 연결할 수있는 내장 panGestureRecognizer가 있습니다. 일반 테이블 스크롤과의 충돌을 피하기 위해 풀다운 해제는 테이블이 맨 위로 스크롤 될 때만 시작됩니다. 스냅 샷을 사용하고 댓글도 많이 추가했습니다.
Robert Chen

로버트, 더 좋은 일. scrollViewDidScroll, scrollViewWillBeginDragging과 같은 기존 tableView 패닝 메서드를 사용하는 자체 구현을 만들었습니다. tableView는 bounces와 bouncesVertically 둘 다 true로 설정해야합니다. 이렇게하면 tableview 항목의 ContentOffset을 측정 할 수 있습니다. 이 방법의 장점은 (바운스로 인해) 충분한 속도가있는 경우 한 번의 제스처로 tableview를 화면에서 스 와이프 할 수 있다는 것입니다. 이번 주에 풀 리퀘스트를 보내드릴 것입니다. 두 옵션 모두 유효 해 보입니다.
Ross Barbish

@RossBarbish 잘하셨습니다. 어떻게했는지보고 싶습니다. 위로 스크롤 한 다음 인터랙티브 전환 모드로 전환 할 수 있다는 것이 매우 멋질 것입니다.
Robert Chen

1
세트 프리젠 테이션 속성 segue으로는 over current context당신이의 ViewController 아래로 당기면 뒷면에 검은 화면을 피하기 위해
nitish005

63

Swift 3에서 어떻게했는지 공유하겠습니다.

결과

이행

class MainViewController: UIViewController {

  @IBAction func click() {
    performSegue(withIdentifier: "showModalOne", sender: nil)
  }
  
}

class ModalOneViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .yellow
  }
  
  @IBAction func click() {
    performSegue(withIdentifier: "showModalTwo", sender: nil)
  }
}

class ModalTwoViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .green
  }
}

Modals View Controller 가 특정 속도에 도달 할 때 드래그 및 해제 할 수 있도록 class빌드 한 ( ViewControllerPannable) 에서 상속하는 곳 입니다 .

ViewControllerPannable 클래스

class ViewControllerPannable: UIViewController {
  var panGestureRecognizer: UIPanGestureRecognizer?
  var originalPosition: CGPoint?
  var currentPositionTouched: CGPoint?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
    view.addGestureRecognizer(panGestureRecognizer!)
  }
  
  func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)
    
    if panGesture.state == .began {
      originalPosition = view.center
      currentPositionTouched = panGesture.location(in: view)
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
          x: translation.x,
          y: translation.y
        )
    } else if panGesture.state == .ended {
      let velocity = panGesture.velocity(in: view)

      if velocity.y >= 1500 {
        UIView.animate(withDuration: 0.2
          , animations: {
            self.view.frame.origin = CGPoint(
              x: self.view.frame.origin.x,
              y: self.view.frame.size.height
            )
          }, completion: { (isCompleted) in
            if isCompleted {
              self.dismiss(animated: false, completion: nil)
            }
        })
      } else {
        UIView.animate(withDuration: 0.2, animations: {
          self.view.center = self.originalPosition!
        })
      }
    }
  }
}

1
나는 당신의 코드를 복사했고 작동했습니다. 풀다운이 검은
Nguyễn Anh Việt

5
스토리 보드에서에서이 검사기 속성 의 패널 Storyboard segueMainViewControllerModalViewController설정 : 프리젠 테이션 에 재산 과전류 컨텍스트
윌슨

이것은 허용되는 대답보다 더 쉬워 보이지만 ViewControllerPannable 클래스에서 오류가 발생합니다. 오류는 "비 함수 유형 UIPanGestureRecognizer의 값을 호출 할 수 없습니다"입니다. "panGestureRecognizer = UIPanGestureRecognizer (target : self, action : #selector (panGestureAction (_ :)))"줄에 있습니다. 아이디어가 있습니까?
tylerSF

UIPanGestureRecognizer로 변경하여 언급 한 오류를 수정했습니다. 변경 "panGestureRecognizer = panGestureRecognizer (목표 ..." "= panGestureRecognizer UIPanGestureRecognizer (목표 ..."
tylerSF

모달로 제시하지 않는 VC를 제시하고 있는데, 해제하는 동안 검정색 배경을 제거하려면 어떻게해야합니까?
미스터 빈

19

다음은 @wilson의 답변 (👍 감사합니다)을 기반으로 한 단일 파일 솔루션이며 다음과 같은 개선 사항이 있습니다.


이전 솔루션의 개선 사항 목록

  • 뷰가 아래로만 내려가도록 패닝을 제한합니다.
    • y좌표 만 업데이트하여 수평 이동을 피하십시오.view.frame.origin
    • 위로 스 와이프 할 때 화면 밖으로 이동하지 마십시오. let y = max(0, translation.y)
  • 또한 스 와이프 속도뿐만 아니라 손가락을 떼는 위치 (기본값은 화면 하단)를 기준으로 뷰 컨트롤러를 닫습니다.
  • 뷰 컨트롤러를 모달로 표시하여 이전 뷰 컨트롤러가 뒤에 나타나고 검정색 배경을 피하도록합니다 (@ nguyễn-anh-việt 질문에 답해야 함)
  • 불필요한 제거 currentPositionTouchedoriginalPosition
  • 다음 매개 변수를 노출하십시오.
    • minimumVelocityToHide: 숨기기에 충분한 속도 (기본값은 1500)
    • minimumScreenRatioToHide: 숨길 수있는 정도 (기본값 : 0.5)
    • animationDuration : 얼마나 빨리 숨기거나 표시합니까 (기본값은 0.2 초)

해결책

Swift 3 및 Swift 4 :

//
//  PannableViewController.swift
//

import UIKit

class PannableViewController: UIViewController {
    public var minimumVelocityToHide: CGFloat = 1500
    public var minimumScreenRatioToHide: CGFloat = 0.5
    public var animationDuration: TimeInterval = 0.2

    override func viewDidLoad() {
        super.viewDidLoad()

        // Listen for pan gesture
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc func onPan(_ panGesture: UIPanGestureRecognizer) {

        func slideViewVerticallyTo(_ y: CGFloat) {
            self.view.frame.origin = CGPoint(x: 0, y: y)
        }

        switch panGesture.state {

        case .began, .changed:
            // If pan started or is ongoing then
            // slide the view to follow the finger
            let translation = panGesture.translation(in: view)
            let y = max(0, translation.y)
            slideViewVerticallyTo(y)

        case .ended:
            // If pan ended, decide it we should close or reset the view
            // based on the final position and the speed of the gesture
            let translation = panGesture.translation(in: view)
            let velocity = panGesture.velocity(in: view)
            let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
                          (velocity.y > minimumVelocityToHide)

            if closing {
                UIView.animate(withDuration: animationDuration, animations: {
                    // If closing, animate to the bottom of the view
                    self.slideViewVerticallyTo(self.view.frame.size.height)
                }, completion: { (isCompleted) in
                    if isCompleted {
                        // Dismiss the view when it dissapeared
                        dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                // If not closing, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    slideViewVerticallyTo(0)
                })
            }

        default:
            // If gesture state is undefined, reset the view to the top
            UIView.animate(withDuration: animationDuration, animations: {
                slideViewVerticallyTo(0)
            })

        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }
}

코드에 오타 또는 누락 된 변수 "minimumHeightRatioToHide"가 있습니다.
shokaveli

1
감사합니다 @shokaveli, 고정 (했다 minimumScreenRatioToHide)
agirault

이것은 아주 좋은 해결책입니다. 그러나 작은 문제가 있으며 원인이 무엇인지 잘 모르겠습니다. dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 빨간색 배경은 모달 VC의 일부이고 파란색 배경은 VC의 일부입니다. 모달을 제시했습니다. 팬 제스처 인식기가 시작될 때 문제가 해결되지 않는 것처럼 보이는 동작이 있습니다.
Knolraap

1
안녕하세요 @Knolraap. 처음 self.view.frame.origin호출하기 전에 의 값을 살펴볼 수 있습니다 sliceViewVerticallyTo. 우리가 보는 오프셋이 상태 표시 줄 높이와 동일한 것 같으므로 초기 원점이 0이 아닐 수도 있습니다.
agirault

1
.NET에서 slideViewVerticallyTo중첩 함수 로 사용 하는 것이 좋습니다 onPan.
Nik Kov

16

snapchat의 검색 모드와 같은 뷰 컨트롤러를 해제하기 위해 대화식으로 아래로 드래그하는 데모를 만들었습니다. 샘플 프로젝트는 이 github 를 확인하십시오 .

여기에 이미지 설명 입력


2
훌륭하지만 정말 구식입니다. 누구든지 이와 같은 다른 샘플 프로젝트를 알고 있습니까?
thelearner

14

Pangesture를 사용하는 Swift 4.x

간단한 방법

세로

class ViewConrtoller: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrage(_:))))
    }

    @objc func onDrage(_ sender:UIPanGestureRecognizer) {
        let percentThreshold:CGFloat = 0.3
        let translation = sender.translation(in: view)

        let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
        let progress = progressAlongAxis(newX, view.bounds.width)

        view.frame.origin.x = newX //Move view to new position

        if sender.state == .ended {
            let velocity = sender.velocity(in: view)
           if velocity.x >= 300 || progress > percentThreshold {
               self.dismiss(animated: true) //Perform dismiss
           } else {
               UIView.animate(withDuration: 0.2, animations: {
                   self.view.frame.origin.x = 0 // Revert animation
               })
          }
       }

       sender.setTranslation(.zero, in: view)
    }
}

도우미 기능

func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
        let movementOnAxis = pointOnAxis / axisLength
        let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
        let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
        return CGFloat(positiveMovementOnAxisPercent)
    }

    func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T : Comparable {
        return min(max(value, minimum), maximum)
    }

어려운 방법

이것을 참조하십시오-> https://github.com/satishVekariya/DraggableViewController


1
귀하의 코드를 사용하려고했습니다. 그러나 내 하위 뷰가 하단에 있고 사용자가 뷰를 끌면 하위 뷰의 높이도 탭된 위치에 대해 증가해야하는 경우 약간 변경하고 싶습니다. 참고 :-하위보기에 배치 된 제스처 이벤트
Ekra

물론, 당신은 그것을 할 수 있습니다
SPatel

안녕하세요 @SPatel이 코드를 수정하여 x 축의 왼쪽에서 드래그하는 데 사용하는 방법에 대한 아이디어가 있습니까?
Nikhil Pandey

1
@SPatel 수직은 x 축 움직임을 나타내고 수평은 y 축 움직임을 나타내므로 답의 제목을 변경해야합니다.
Nikhil Pandey

1
modalPresentationStyle = UIModalPresentationOverFullScreen뒤 화면을 피하기 위해 설정 해야합니다 view.
tounaobun

13

이 작업을 수행하는 매우 간단한 방법을 알아 냈습니다. 뷰 컨트롤러에 다음 코드를 넣으십시오.

스위프트 4

override func viewDidLoad() {
    super.viewDidLoad()
    let gestureRecognizer = UIPanGestureRecognizer(target: self,
                                                   action: #selector(panGestureRecognizerHandler(_:)))
    view.addGestureRecognizer(gestureRecognizer)
}

@IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
    let touchPoint = sender.location(in: view?.window)
    var initialTouchPoint = CGPoint.zero

    switch sender.state {
    case .began:
        initialTouchPoint = touchPoint
    case .changed:
        if touchPoint.y > initialTouchPoint.y {
            view.frame.origin.y = touchPoint.y - initialTouchPoint.y
        }
    case .ended, .cancelled:
        if touchPoint.y - initialTouchPoint.y > 200 {
            dismiss(animated: true, completion: nil)
        } else {
            UIView.animate(withDuration: 0.2, animations: {
                self.view.frame = CGRect(x: 0,
                                         y: 0,
                                         width: self.view.frame.size.width,
                                         height: self.view.frame.size.height)
            })
        }
    case .failed, .possible:
        break
    }
}

1
감사합니다, 완벽하게 작동합니다! Pan Gesture Recogniser를 인터페이스 빌더의보기에 놓고 위의 @IBAction과 연결하기 만하면됩니다.
balazs630

Swift 5에서도 작동합니다. @ balazs630이 준 지침을 따르십시오.
LondonGuy

이것이 최선의 방법이라고 생각합니다.
Enkha

@Alex Shubin, ViewController에서 TabbarController로 끌 때 닫는 방법은 무엇입니까?
Vadlapalli Masthan

12

Swift 4 의 저장소를 대규모로 업데이트합니다 .

들어 스위프트 3 , 내가 존재하는에 다음을 생성 한 UIViewController오른쪽에서 왼쪽으로 팬 동작하여 기각한다. 이것을 GitHub 저장소 로 업로드했습니다 .

여기에 이미지 설명 입력

DismissOnPanGesture.swift 파일:

//  Created by David Seek on 11/21/16.
//  Copyright © 2016 David Seek. All rights reserved.

import UIKit

class DismissAnimator : NSObject {
}

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let screenBounds = UIScreen.main.bounds
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        var x:CGFloat      = toVC!.view.bounds.origin.x - screenBounds.width
        let y:CGFloat      = toVC!.view.bounds.origin.y
        let width:CGFloat  = toVC!.view.bounds.width
        let height:CGFloat = toVC!.view.bounds.height
        var frame:CGRect   = CGRect(x: x, y: y, width: width, height: height)

        toVC?.view.alpha = 0.2

        toVC?.view.frame = frame
        let containerView = transitionContext.containerView

        containerView.insertSubview(toVC!.view, belowSubview: fromVC!.view)


        let bottomLeftCorner = CGPoint(x: screenBounds.width, y: 0)
        let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)

        UIView.animate(
            withDuration: transitionDuration(using: transitionContext),
            animations: {
                fromVC!.view.frame = finalFrame
                toVC?.view.alpha = 1

                x = toVC!.view.bounds.origin.x
                frame = CGRect(x: x, y: y, width: width, height: height)

                toVC?.view.frame = frame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        )
    }
}

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

let transition: CATransition = CATransition()

func presentVCRightToLeft(_ fromVC: UIViewController, _ toVC: UIViewController) {
    transition.duration = 0.5
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromRight
    fromVC.view.window!.layer.add(transition, forKey: kCATransition)
    fromVC.present(toVC, animated: false, completion: nil)
}

func dismissVCLeftToRight(_ vc: UIViewController) {
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromLeft
    vc.view.window!.layer.add(transition, forKey: nil)
    vc.dismiss(animated: false, completion: nil)
}

func instantiatePanGestureRecognizer(_ vc: UIViewController, _ selector: Selector) {
    var edgeRecognizer: UIScreenEdgePanGestureRecognizer!
    edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: vc, action: selector)
    edgeRecognizer.edges = .left
    vc.view.addGestureRecognizer(edgeRecognizer)
}

func dismissVCOnPanGesture(_ vc: UIViewController, _ sender: UIScreenEdgePanGestureRecognizer, _ interactor: Interactor) {
    let percentThreshold:CGFloat = 0.3
    let translation = sender.translation(in: vc.view)
    let fingerMovement = translation.x / vc.view.bounds.width
    let rightMovement = fmaxf(Float(fingerMovement), 0.0)
    let rightMovementPercent = fminf(rightMovement, 1.0)
    let progress = CGFloat(rightMovementPercent)

    switch sender.state {
    case .began:
        interactor.hasStarted = true
        vc.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
    }
}

쉬운 사용법 :

import UIKit

class VC1: UIViewController, UIViewControllerTransitioningDelegate {

    let interactor = Interactor()

    @IBAction func present(_ sender: Any) {
        let vc = self.storyboard?.instantiateViewController(withIdentifier: "VC2") as! VC2
        vc.transitioningDelegate = self
        vc.interactor = interactor

        presentVCRightToLeft(self, vc)
    }

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

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

class VC2: UIViewController {

    var interactor:Interactor? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        instantiatePanGestureRecognizer(self, #selector(gesture))
    }

    @IBAction func dismiss(_ sender: Any) {
        dismissVCLeftToRight(self)
    }

    func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
        dismissVCOnPanGesture(self, sender, interactor!)
    }
}

1
굉장 해요! 공유해 주셔서 감사합니다.
Sharad Chauhan

안녕, 어떻게 사용하여 표시 할 수 있습니다 팬 제스처 인식기는 도움말 나 덕분에 기쁘게
Muralikrishna

지금 튜토리얼이있는 유튜브 채널을 시작하고 있습니다. iOS 13 이상 / Swift 5
David Seek

6

설명하는 것은 대화 형 사용자 지정 전환 애니메이션 입니다. 전환의 애니메이션과 구동 제스처, 즉 제시된 뷰 컨트롤러의 해제 (또는 해제)를 모두 사용자 정의하고 있습니다. 이를 구현하는 가장 쉬운 방법은 UIPanGestureRecognizer를 UIPercentDrivenInteractiveTransition과 결합하는 것입니다.

내 책은이를 수행하는 방법을 설명하고 책에서 예제를 게시했습니다. 이 특정 예제는 다른 상황입니다. 전환은 아래가 아닌 옆으로 진행되며, 제시된 컨트롤러가 아닌 탭 막대 컨트롤러에 대한 것입니다.하지만 기본 아이디어는 정확히 동일합니다.

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p296customAnimation2/ch19p620customAnimation1/AppDelegate.swift

해당 프로젝트를 다운로드하고 실행하면 진행중인 작업이 옆으로 있다는 점을 제외하고 정확히 설명하는 내용임을 알 수 있습니다. 드래그가 절반 이상이면 전환하지만 그렇지 않은 경우 취소하고 다시 스냅합니다. 장소.


3
404 페이지를 찾을 수 없음.
사냥꾼

6

수직 해제 만

func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)

    if panGesture.state == .began {
        originalPosition = view.center
        currentPositionTouched = panGesture.location(in: view)    
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
            x:  view.frame.origin.x,
            y:  view.frame.origin.y + translation.y
        )
        panGesture.setTranslation(CGPoint.zero, in: self.view)
    } else if panGesture.state == .ended {
        let velocity = panGesture.velocity(in: view)
        if velocity.y >= 150 {
            UIView.animate(withDuration: 0.2
                , animations: {
                    self.view.frame.origin = CGPoint(
                        x: self.view.frame.origin.x,
                        y: self.view.frame.size.height
                    )
            }, completion: { (isCompleted) in
                if isCompleted {
                    self.dismiss(animated: false, completion: nil)
                }
            })
        } else {
            UIView.animate(withDuration: 0.2, animations: {
                self.view.center = self.originalPosition!
            })
        }
    }

6

사용하기 쉬운 확장 프로그램을 만들었습니다.

InteractiveViewController를 사용하여 UIViewController를 고유하게 사용하면 InteractiveViewController 가 완료됩니다.

컨트롤러에서 showInteractive () 메서드를 호출하여 Interactive로 표시합니다.

여기에 이미지 설명 입력


4

Objective C에서 : 다음은 코드입니다.

viewDidLoad

UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
                                             initWithTarget:self action:@selector(swipeDown:)];
swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[self.view addGestureRecognizer:swipeRecognizer];

//Swipe Down Method

- (void)swipeDown:(UIGestureRecognizer *)sender{
[self dismissViewControllerAnimated:YES completion:nil];
}

닫히기 전에 아래로 스 와이프하는 시간을 제어하는 ​​방법은 무엇입니까?
TonyTony

2

@Wilson 답변을 기반으로 만든 확장은 다음과 같습니다.

// MARK: IMPORT STATEMENTS
import UIKit

// MARK: EXTENSION
extension UIViewController {

    // MARK: IS SWIPABLE - FUNCTION
    func isSwipable() {
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        self.view.addGestureRecognizer(panGestureRecognizer)
    }

    // MARK: HANDLE PAN GESTURE - FUNCTION
    @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: view)
        let minX = view.frame.width * 0.135
        var originalPosition = CGPoint.zero

        if panGesture.state == .began {
            originalPosition = view.center
        } else if panGesture.state == .changed {
            view.frame.origin = CGPoint(x: translation.x, y: 0.0)

            if panGesture.location(in: view).x > minX {
                view.frame.origin = originalPosition
            }

            if view.frame.origin.x <= 0.0 {
                view.frame.origin.x = 0.0
            }
        } else if panGesture.state == .ended {
            if view.frame.origin.x >= view.frame.width * 0.5 {
                UIView.animate(withDuration: 0.2
                     , animations: {
                        self.view.frame.origin = CGPoint(
                            x: self.view.frame.size.width,
                            y: self.view.frame.origin.y
                        )
                }, completion: { (isCompleted) in
                    if isCompleted {
                        self.dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                UIView.animate(withDuration: 0.2, animations: {
                    self.view.frame.origin = originalPosition
                })
            }
        }
    }

}

용법

뷰 컨트롤러 내에서 스 와이프 할 수 있습니다.

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

내비게이션 컨트롤러로서 뷰 컨트롤러의 맨 왼쪽에서 스 와이프하면 닫힐 수 있습니다.


이봐, 나는 당신의 코드를 사용했고 그것은 오른쪽 스 와이프에 완벽하게 작동하지만 아래로 스 와이프에서 해제하고 싶습니다. 어떻게 할 수 있습니까? 도와주세요!
Khush

2

이것은 축에서 드래그 ViewController에 대한 내 간단한 클래스입니다 . 그냥 herited DraggableViewController에서 클래스를.

MyCustomClass: DraggableViewController

제시된 ViewController에서만 작동합니다.

// MARK: - DraggableViewController

public class DraggableViewController: UIViewController {

    public let percentThresholdDismiss: CGFloat = 0.3
    public var velocityDismiss: CGFloat = 300
    public var axis: NSLayoutConstraint.Axis = .horizontal
    public var backgroundDismissColor: UIColor = .black {
        didSet {
            navigationController?.view.backgroundColor = backgroundDismissColor
        }
    }

    // MARK: LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrag(_:))))
    }

    // MARK: Private methods

    @objc fileprivate func onDrag(_ sender: UIPanGestureRecognizer) {

        let translation = sender.translation(in: view)

        // Movement indication index
        let movementOnAxis: CGFloat

        // Move view to new position
        switch axis {
        case .vertical:
            let newY = min(max(view.frame.minY + translation.y, 0), view.frame.maxY)
            movementOnAxis = newY / view.bounds.height
            view.frame.origin.y = newY

        case .horizontal:
            let newX = min(max(view.frame.minX + translation.x, 0), view.frame.maxX)
            movementOnAxis = newX / view.bounds.width
            view.frame.origin.x = newX
        }

        let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
        let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
        let progress = CGFloat(positiveMovementOnAxisPercent)
        navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(1 - progress)

        switch sender.state {
        case .ended where sender.velocity(in: view).y >= velocityDismiss || progress > percentThresholdDismiss:
            // After animate, user made the conditions to leave
            UIView.animate(withDuration: 0.2, animations: {
                switch self.axis {
                case .vertical:
                    self.view.frame.origin.y = self.view.bounds.height

                case .horizontal:
                    self.view.frame.origin.x = self.view.bounds.width
                }
                self.navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(0)

            }, completion: { finish in
                self.dismiss(animated: true) //Perform dismiss
            })
        case .ended:
            // Revert animation
            UIView.animate(withDuration: 0.2, animations: {
                switch self.axis {
                case .vertical:
                    self.view.frame.origin.y = 0

                case .horizontal:
                    self.view.frame.origin.x = 0
                }
            })
        default:
            break
        }
        sender.setTranslation(.zero, in: view)
    }
}

2

Custom UIViewController Transition에 대해 좀 더 자세히 알아보고 싶은 분들을 위해 raywenderlich.com의이 훌륭한 튜토리얼을 추천 합니다.

원래 최종 샘플 프로젝트에는 버그가 포함되어 있습니다. 그래서 수정하고 Github repo에 업로드했습니다 . proj는 Swift 5에 있으므로 쉽게 실행하고 재생할 수 있습니다.

다음은 미리보기입니다.

그리고 그것은 또한 상호 작용 적입니다!

해피 해킹!


0

UIPanGestureRecognizer를 사용하여 사용자의 드래그를 감지하고 함께 모달보기를 이동할 수 있습니다. 종료 위치가 충분히 아래에있는 경우보기를 닫거나 원래 위치로 다시 애니메이션 할 수 있습니다.

이와 같은 구현 방법에 대한 자세한 내용은 이 답변 을 확인하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.