Powszechnym sposobem na odrzucenie modalu jest przesunięcie w dół - jak możemy pozwolić użytkownikowi przeciągnąć modal w dół, jeśli jest wystarczająco daleko, modal zostaje odrzucony, w przeciwnym razie animuje się z powrotem do pierwotnej pozycji?
Na przykład możemy znaleźć to używane w widokach zdjęć w aplikacji Twitter lub w trybie „odkrywania” Snapchata.
Podobne wątki wskazują, że możemy użyć UISwipeGestureRecognizer i [self discissViewControllerAnimated ...], aby odrzucić modalne VC, gdy użytkownik przesuwa w dół. Ale to obsługuje tylko jedno przesunięcie, nie pozwalając użytkownikowi przeciągać modalu.
Odpowiedzi:
Właśnie stworzyłem samouczek do interaktywnego przeciągania w dół modalu, aby go odrzucić.
http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/
Na początku stwierdziłem, że ten temat jest zagmatwany, więc samouczek buduje to krok po kroku.
Jeśli chcesz samemu uruchomić kod, oto repozytorium:
https://github.com/ThornTechPublic/InteractiveModal
Oto podejście, które zastosowałem:
Wyświetl kontroler
Zastępujesz animację zamknięcia niestandardową. Jeśli użytkownik przeciąga modal,
interactor
zaczyna działać.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 } }
Zamknij Animator
Tworzysz niestandardowy animator. To jest niestandardowa animacja umieszczona w
UIViewControllerAnimatedTransitioning
protokole.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()) } ) } }
Interactor
Tworzysz podklasę,
UIPercentDrivenInteractiveTransition
aby mogła działać jako twoja maszyna stanu. Ponieważ do obiektu międzyaktorowego mają dostęp oba VC, użyj go do śledzenia postępu panoramowania.import UIKit class Interactor: UIPercentDrivenInteractiveTransition { var hasStarted = false var shouldFinish = false }
Kontroler widoku modalnego
Powoduje to odwzorowanie stanu gestu panoramy na wywołania metod interactor.
translationInView()
y
Wartość określa, czy użytkownik przekroczył próg. Kiedy gest przesunięcia jest.Ended
, interaktor kończy lub anuluje.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 } } }
źródło
segue
na,over current context
aby uniknąć czarnego ekranu z tyłu poPodzielę się tym, jak zrobiłem to w Swift 3:
Wynik
Realizacja
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 } }
Gdzie modalne kontrolery widoku dziedziczą po
class
zbudowanym przeze mnie (ViewControllerPannable
), aby uczynić je przeciągalnymi i odrzucalnymi po osiągnięciu określonej prędkości.ViewControllerPannable, klasa
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! }) } } } }
źródło
Storyboard segue
ofMainViewController
toModalViewController
: ustaw właściwość Presentation na Over Current ContextOto jednoplikowe rozwiązanie oparte na odpowiedzi @ wilson (dzięki 👍) z następującymi ulepszeniami:
Lista ulepszeń z poprzedniego rozwiązania
y
współrzędneview.frame.origin
let y = max(0, translation.y)
currentPositionTouched
ioriginalPosition
minimumVelocityToHide
: jaka prędkość jest wystarczająca do ukrycia (domyślnie 1500)minimumScreenRatioToHide
: jak nisko wystarczy, aby się ukryć (domyślnie 0,5)animationDuration
: jak szybko ukrywamy / pokazujemy (domyślnie 0.2s)Rozwiązanie
Swift 3 i 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; } }
źródło
minimumScreenRatioToHide
)self.view.frame.origin
przedsliceViewVerticallyTo
pierwszym wywołaniem : wydaje się, że przesunięcie, które widzimy, jest takie samo jak wysokość paska stanu, więc może twoje początkowe źródło nie jest 0?slideViewVerticallyTo
jako funkcji zagnieżdżonej wonPan
.stworzył wersję demonstracyjną do interaktywnego przeciągania w dół, aby zamknąć kontroler widoku, taki jak tryb odkrywania snapchata. Sprawdź ten github, aby zobaczyć przykładowy projekt.
źródło
Swift 4.x, używając Pangesture
Prosta droga
Pionowy
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) } }
Funkcja pomocnicza
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) }
Trudny sposób
Skorzystaj z tego -> https://github.com/satishVekariya/DraggableViewController
źródło
modalPresentationStyle = UIModalPresentationOverFullScreen
aby uniknąć tylnego ekranu zaview
.Wymyśliłem super prosty sposób na zrobienie tego. Po prostu umieść następujący kod w kontrolerze widoku:
Szybki 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 } }
źródło
Masowo aktualizuje repozytorium dla Swift 4 .
W przypadku Swift 3 utworzyłem następujący fragment, aby przedstawić
UIViewController
od prawej do lewej i odrzucić go gestem przesuwania. Przesłałem to jako repozytorium GitHub .DismissOnPanGesture.swift
plik:// 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 } }
Łatwe użytkowanie:
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!) } }
źródło
To, co opisujesz, to interaktywna animacja niestandardowego przejścia . Dostosowujesz zarówno animację, jak i gest sterujący przejściem, tj. Zwolnienie (lub nie) prezentowanego kontrolera widoku. Najłatwiejszym sposobem jego zaimplementowania jest połączenie UIPanGestureRecognizer z UIPercentDrivenInteractiveTransition.
Moja książka wyjaśnia, jak to zrobić, i zamieściłem przykłady (z książki). Ten konkretny przykład to inna sytuacja - przejście jest na boki, a nie w dół i dotyczy kontrolera z zakładkami, a nie prezentowanego kontrolera - ale podstawowa idea jest dokładnie taka sama:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p296customAnimation2/ch19p620customAnimation1/AppDelegate.swift
Jeśli pobierzesz ten projekt i uruchomisz go, zobaczysz, że to, co się dzieje, jest dokładnie tym, co opisujesz, z wyjątkiem tego, że jest to bok: jeśli opór jest większy niż połowa, przechodzimy, ale jeśli nie, anulujemy i wracamy do miejsce.
źródło
Tylko odrzucenie pionowe
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! }) } }
źródło
Stworzyłem łatwe w użyciu rozszerzenie.
Po prostu wbudowany w Twój UIViewController z InteractiveViewController i gotowe InteractiveViewController
wywołaj metodę showInteractive () ze swojego kontrolera, aby wyświetlić ją jako interaktywną.
źródło
W celu C: Oto kod
w
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]; }
źródło
Oto rozszerzenie, które zrobiłem na podstawie odpowiedzi @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 }) } } } }
STOSOWANIE
W kontrolerze widoku, który chcesz przesuwać:
override func viewDidLoad() { super.viewDidLoad() self.isSwipable() }
i będzie można go zamknąć, przesuwając palcem od skrajnej lewej strony kontrolera widoku, jako kontroler nawigacji.
źródło
To moja prosta klasa dla Drag ViewController from axis . Właśnie odziedziczył twoją klasę z DraggableViewController.
MyCustomClass: DraggableViewController
Działa tylko dla przedstawionego 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) } }
źródło
Dla tych, którzy naprawdę chcą zanurzyć się trochę głębiej w Custom UIViewController Transition, polecam ten świetny samouczek z raywenderlich.com .
Oryginalny, ostateczny projekt przykładowy zawiera błąd. Naprawiłem to i wgrałem do repozytorium Github . Proj jest w Swift 5, więc możesz łatwo uruchomić i grać.
Oto podgląd:
Jest też interaktywny!
Miłego hakowania!
źródło
Możesz użyć UIPanGestureRecognizer, aby wykryć przeciągnięcie użytkownika i przenieść z nim widok modalny. Jeśli pozycja końcowa jest wystarczająco nisko, widok można odrzucić lub w inny sposób animować z powrotem do pierwotnej pozycji.
Sprawdź tę odpowiedź, aby uzyskać więcej informacji o tym, jak wdrożyć coś takiego.
źródło