W iOS, jak przeciągnąć w dół, aby odrzucić modal?

91

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.

foobar
źródło
Przyjrzyj się niestandardowym przejściom interaktywnym. W ten sposób możesz to zaimplementować. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX
Odwołane do repozytorium github.com/ThornTechPublic/InteractiveModal przez Roberta Chena i napisane klasy wrapper / handler do obsługi wszystkiego. Żaden standardowy kod nie obsługuje czterech podstawowych przejść (od góry do dołu, od dołu do góry, od lewej do prawej i od prawej do lewej) z gestami odrzucającymi github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/ ...
Chamira Fernando
@ChamiraFernando, przyjrzał się Twojemu kodowi i bardzo pomaga. Czy istnieje sposób, aby uwzględnić wiele kierunków zamiast jednego?
Jevon Cowell
Zrobię to. W dzisiejszych czasach czas jest ogromnym ograniczeniem :(
Chamira Fernando

Odpowiedzi:

93

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.

wprowadź opis obrazu tutaj

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, interactorzaczyna 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 UIViewControllerAnimatedTransitioningprotokole.

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ę, UIPercentDrivenInteractiveTransitionaby 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() yWartość 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
        }
    }

}
Robert Chen
źródło
4
Hej Robert, świetna robota. Czy masz pomysł, jak moglibyśmy to zmodyfikować, aby działało z widokami tabeli? To znaczy, że można pociągnąć w dół, aby odrzucić, gdy widok stołu jest na górze? Dzięki
Ross Barbish
1
Ross, stworzyłem nowy oddział, który ma działający przykład: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Jeśli chcesz najpierw zobaczyć, jak to wygląda, obejrzyj ten GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . Widok tabeli ma wbudowaną funkcję panGestureRecognizer, którą można połączyć z istniejącą metodą handleGesture (_ :) za pośrednictwem akcji docelowej. Aby uniknąć konfliktu z normalnym przewijaniem stołu, zwolnienie typu pull-down rozpoczyna się tylko wtedy, gdy stół jest przewijany na górę. Użyłem migawki i dodałem również wiele komentarzy.
Robert Chen
Robercie, więcej świetnej roboty. Zrobiłem własną implementację, która używa istniejących metod panoramowania tableView, takich jak scrollViewDidScroll, scrollViewWillBeginDragging. Wymaga to, aby tableView miał odbijania i bouncesVertically oba ustawione na true - w ten sposób możemy zmierzyć ContentOffset elementów tableview. Zaletą tej metody jest to, że wydaje się, że widok tabeli można zrzucić z ekranu jednym gestem, jeśli prędkość jest wystarczająca (ze względu na odbijanie). Prawdopodobnie wyślę ci żądanie ściągnięcia kiedyś w tym tygodniu, obie opcje wydają się prawidłowe.
Ross Barbish
Dobra robota @ RossBarbish, nie mogę się doczekać, żeby zobaczyć, jak ci się to udało. Będzie fajnie móc przewijać w górę, a następnie przejść do interaktywnego trybu przejścia, a wszystko to jednym płynnym ruchem.
Robert Chen
1
ustaw właściwość prezentacji seguena, over current contextaby uniknąć czarnego ekranu z tyłu po
rozłożeniu viewController
63

Podzielę 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 classzbudowanym 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!
        })
      }
    }
  }
}
Wilson
źródło
1
Skopiowałem twój kod i zadziałało. Bu tło widoku modelu po opuszczeniu jest czarne, nieprzezroczyste jak twoje
Nguyễn Anh Việt
5
W scenorysie z panelu Attributes InspectorStoryboard segue of MainViewControllerto ModalViewController: ustaw właściwość Presentation na Over Current Context
Wilson,
Wydaje się to łatwiejsze niż zaakceptowana odpowiedź, ale otrzymuję błąd w klasie ViewControllerPannable. Błąd to „nie można wywołać wartości typu niefunkcyjnego UIPanGestureRecognizer”. Znajduje się w wierszu "panGestureRecognizer = UIPanGestureRecognizer (target: self, action: #selector (panGestureAction (_ :)))" Jakieś pomysły?
tylerSF
Naprawiono błąd, o którym wspomniałem, zmieniając go na UIPanGestureRecognizer. „panGestureRecognizer = panGestureRecognizer (target ...”) zmieniono na: „panGestureRecognizer = UIPanGestureRecognizer (target ...”
tylerSF
Ponieważ przedstawiam VC, a nie modalnie, jak mogę usunąć czarne tło podczas zamykania?
Jaś Fasola
20

Oto jednoplikowe rozwiązanie oparte na odpowiedzi @ wilson (dzięki 👍) z następującymi ulepszeniami:


Lista ulepszeń z poprzedniego rozwiązania

  • Ogranicz panoramowanie, aby widok był tylko w dół:
    • Unikaj tłumaczenia poziomego, aktualizując tylko ywspółrzędneview.frame.origin
    • Unikaj przesuwania poza ekran podczas przesuwania palcem w górę let y = max(0, translation.y)
  • Odrzuć także kontroler widoku w oparciu o miejsce zwolnienia palca (domyślnie dolna połowa ekranu), a nie tylko na podstawie prędkości przesunięcia
  • Pokaż kontroler widoku jako modalny, aby upewnić się, że poprzedni kontroler widoku pojawia się z tyłu i uniknąć czarnego tła (powinien odpowiedzieć na twoje pytanie @ nguyễn-anh-việt)
  • Usuń niepotrzebne currentPositionTouched ioriginalPosition
  • Uwidocznij następujące parametry:
    • 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;
    }
}
agirault
źródło
W Twoim kodzie jest literówka lub brakująca zmienna „minimumHeightRatioToHide”
shokaveli,
1
Dzięki @shokaveli, naprawiono (było minimumScreenRatioToHide)
agirault
To bardzo fajne rozwiązanie. Mam jednak mały problem i nie jestem do końca pewien, jaka jest przyczyna: dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 Czerwone tło jest częścią modalnej VC, niebieskie tło jest częścią VC, która przedstawił modalne. Występuje to błędne zachowanie, gdy uruchamia się aparat rozpoznawania gestów przesuwania, którego nie mogę naprawić.
Knolraap,
1
Cześć @Knolraap. Może spójrz na wartość self.view.frame.originprzed sliceViewVerticallyTopierwszym 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?
agirault,
1
Lepiej jest używać slideViewVerticallyTojako funkcji zagnieżdżonej w onPan.
Nik Kov
16

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.

wprowadź opis obrazu tutaj

med
źródło
2
Świetnie, ale jest naprawdę przestarzały. Czy ktoś zna inny przykładowy projekt, taki jak ten?
thelearner
14

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

SPatel
źródło
1
Próbowałem użyć twojego kodu. ale chcę wprowadzić niewielką zmianę, gdy mój widok podrzędny znajduje się na dole, a gdy użytkownik przeciąga widok, wysokość widoku podrzędnego powinna również wzrosnąć w odniesieniu do pozycji dotkniętej. Uwaga: - zdarzenie gestu zostało umieszczone w
podglądzie
Jasne, możesz to zrobić
SPatel
Cześć @SPatel Masz jakiś pomysł, jak zmodyfikować ten kod, aby używać go do przeciągania po lewej stronie osi x, tj. E, ujemne ruchy wzdłuż osi x?
Nikhil Pandey,
1
@SPatel Musisz także zmienić nagłówek odpowiedzi, ponieważ pionowy przedstawia ruchy w osi x, a poziomy przedstawia ruchy w osi y.
Nikhil Pandey
1
Pamiętaj, aby ustawić, modalPresentationStyle = UIModalPresentationOverFullScreenaby uniknąć tylnego ekranu za view.
tounaobun
13

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
    }
}
Alex Shubin
źródło
1
Dzięki, działa idealnie! Po prostu upuść Pan Gesture Recogniser do widoku w narzędziu do tworzenia interfejsów i połącz się z powyższym @IBAction.
balazs630
Działa również w Swift 5. Po prostu postępuj zgodnie z instrukcjami podanymi przez @ balazs630.
LondonGuy
Myślę, że to najlepszy sposób.
Enkha
@Alex Shubin, Jak odrzucić podczas przeciągania z ViewController do TabbarController?
Vadlapalli Masthan
12

Masowo aktualizuje repozytorium dla Swift 4 .

W przypadku Swift 3 utworzyłem następujący fragment, aby przedstawić UIViewControllerod prawej do lewej i odrzucić go gestem przesuwania. Przesłałem to jako repozytorium GitHub .

wprowadź opis obrazu tutaj

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!)
    }
}
David Seek
źródło
1
Po prostu super! Dzięki za udostępnienie.
Sharad Chauhan
Cześć, jak mogę prezentować za pomocą narzędzia Pan Gesture Recognizer, pomóż mi, dziękuję
Muralikrishna
Rozpoczynam teraz kanał youtube z samouczkami. Mógłbym stworzyć odcinek opisujący ten problem na iOS 13+ / Swift 5
David Seek
6

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.

matowe
źródło
3
404 Nie znaleziono strony.
trapper
6

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!
            })
        }
    }
miss Gbot
źródło
6

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ą.

wprowadź opis obrazu tutaj

Shoaib
źródło
4

W celu C: Oto kod

wviewDidLoad

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];
}
Deepak Kumar
źródło
jak kontrolować czas przesunięcia w dół, zanim zostanie odrzucony?
TonyTony
2

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.

fredericdnd
źródło
Hej, użyłem Twojego kodu i działa idealnie w przypadku prawego przesunięcia, ale chcę odrzucić po przesunięciu w dół, jak mogę to zrobić? Proszę pomóż!
Khush
2

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)
    }
}
YannSteph
źródło
2

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!

Benjamin Wen
źródło
0

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.

DanielG
źródło