No iOS, como arrastar para baixo para dispensar um modal?


90

Uma maneira comum de dispensar um modal é deslizar para baixo - Como permitimos que o usuário arraste o modal para baixo, se estiver longe o suficiente, o modal é dispensado, caso contrário, ele volta à posição original?

Por exemplo, podemos encontrar isso usado nas visualizações de fotos do aplicativo do Twitter ou no modo "descobrir" do Snapchat.

Tópicos semelhantes indicam que podemos usar um UISwipeGestureRecognizer e [self CC0ViewControllerAnimated ...] para descartar um VC modal quando um usuário desliza para baixo. Mas isso só lida com um único toque, não permitindo que o usuário arraste o modal.


Dê uma olhada nas transições interativas personalizadas. Esta é a maneira como você pode implementá-lo. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX

Referido a github.com/ThornTechPublic/InteractiveModal repo por Robert Chen e escreveu uma classe wrapper / manipulador para lidar com tudo. O código padrão não suporta mais quatro transições básicas (de cima para baixo, de baixo para cima, da esquerda para a direita e da direita para a esquerda) com gestos de dispensa github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/…
Chamira Fernando

@ChamiraFernando, olhou o seu código e ajudou muito. Existe uma maneira de fazer com que várias direções sejam incluídas em vez de uma?
Jevon Cowell

Eu farei. O tempo é uma grande limitação nos dias de hoje :(
Chamira Fernando

Respostas:


93

Acabei de criar um tutorial para arrastar interativamente um modal para descartá-lo.

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

Achei este tópico confuso no início, então o tutorial desenvolve isso passo a passo.

insira a descrição da imagem aqui

Se você deseja apenas executar o código sozinho, este é o repo:

https://github.com/ThornTechPublic/InteractiveModal

Esta é a abordagem que usei:

Ver controlador

Você substitui a animação dispensada por uma personalizada. Se o usuário estiver arrastando o modal, ele interactorentra em ação.

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
    }
}

Dispensar animador

Você cria um animador personalizado. Esta é uma animação personalizada que você empacota dentro de um UIViewControllerAnimatedTransitioningprotocolo.

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())
            }
        )
    }
}

Interator

Você cria UIPercentDrivenInteractiveTransitionuma subclasse para que ela possa atuar como sua máquina de estado. Como o objeto interagente é acessado por ambos os VCs, use-o para controlar o progresso da panorâmica.

import UIKit

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

Modal View Controller

Isso mapeia o estado do gesto panorâmico para chamadas de método do interator. O translationInView() yvalor determina se o usuário ultrapassou um limite. Quando o gesto de panorâmica é .Ended, o interator termina ou cancela.

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
Ei, Robert, ótimo trabalho. Você tem uma ideia de como poderíamos modificar isso para permitir que funcione com visualizações de tabela? Ou seja, poder puxar para baixo para dispensar quando o tableview estiver no topo? Obrigado
Ross Barbish,

1
Ross, eu criei uma nova filial que tem um exemplo funcional: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Se você quiser ver como fica primeiro, verifique este GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . A visualização da tabela possui um panGestureRecognizer integrado que pode ser conectado ao método handleGesture (_ :) existente por meio da ação de destino. Para evitar conflito com a rolagem normal da tabela, a dispensa pull-down só entra em ação quando a tabela é rolada para o topo. Usei um instantâneo e adicionei muitos comentários também.
Robert Chen

Robert, mais um ótimo trabalho. Eu fiz minha própria implementação que usa os métodos de panning tableView existentes, como scrollViewDidScroll, scrollViewWillBeginDragging. Exige que tableView tenha bounces e bouncesVertically configurados como true - dessa forma, podemos medir o ContentOffset dos itens de tableview. A vantagem desse método é que ele parece permitir que o tableview seja deslizado para fora da tela em um gesto se houver velocidade suficiente (devido ao salto). Provavelmente enviarei uma solicitação de pull ainda esta semana, ambas as opções parecem válidas.
Ross Barbish

Bom trabalho @RossBarbish, mal posso esperar para ver como você conseguiu. Será muito bom poder rolar para cima e entrar no modo de transição interativo, tudo em um movimento fluido.
Robert Chen

1
defina a propriedade de apresentação de seguecomo over current contextpara evitar a tela preta na parte de trás ao puxar o viewController
nitish005

62

Vou compartilhar como fiz isso no Swift 3:

Resultado

Implementação

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
  }
}

Onde os Modals View Controllers herdam de um classque eu construí ( ViewControllerPannable) para torná-los arrastáveis ​​e dispensáveis ​​quando atingem certa velocidade.

Classe 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
Copiei seu código e funcionou. O fundo da visualização do modelo quando puxado para baixo é preto, não transparente como o seu
Nguyễn Anh Việt

5
No storyboard do painel Attributes InspectorStoryboard segue de MainViewControllerpara ModalViewController: defina a propriedade Presentation para Over Current Context
Wilson

Parece mais fácil do que a resposta aceita, mas estou recebendo um erro na classe ViewControllerPannable. O erro é "não é possível chamar o valor do tipo não funcional UIPanGestureRecognizer". Está na linha "panGestureRecognizer = UIPanGestureRecognizer (target: self, action: #selector (panGestureAction (_ :)))" Alguma ideia?
tylerSF

Corrigido o erro que mencionei alterando-o para UIPanGestureRecognizer. "panGestureRecognizer = panGestureRecognizer (target ..." alterado para: "panGestureRecognizer = UIPanGestureRecognizer (target ..."
tylerSF

Já que estou apresentando o VC não de forma modal, como posso remover o fundo preto ao dispensar?
Sr. Bean

18

Aqui está uma solução de um arquivo com base na resposta de @ wilson (obrigado 👍) com as seguintes melhorias:


Lista de melhorias da solução anterior

  • Limite a panorâmica para que a visualização apenas diminua:
    • Evite a translação horizontal atualizando apenas a ycoordenada deview.frame.origin
    • Evite girar para fora da tela ao deslizar para cima com let y = max(0, translation.y)
  • Dispense também o controlador de visualização com base em onde o dedo é liberado (o padrão é a metade inferior da tela) e não apenas com base na velocidade de deslize
  • Mostre o controlador de visão como modal para garantir que o controlador de visão anterior apareça atrás e evite um fundo preto (deve responder sua pergunta @ nguyễn-anh-việt)
  • Remova desnecessários currentPositionTouchedeoriginalPosition
  • Exponha os seguintes parâmetros:
    • minimumVelocityToHide: qual velocidade é suficiente para ocultar (o padrão é 1500)
    • minimumScreenRatioToHide: quão baixo é o suficiente para ocultar (o padrão é 0,5)
    • animationDuration : quão rápido nós escondemos / mostramos (o padrão é 0.2s)

Solução

Swift 3 e 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;
    }
}

Em seu código há um erro de digitação ou uma variável ausente "minimumHeightRatioToHide"
shokaveli

1
Obrigado @shokaveli, consertou (era minimumScreenRatioToHide)
agirault

Esta é uma solução muito boa. No entanto, tenho um pequeno problema e não tenho certeza de qual é a causa: dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 O fundo vermelho faz parte do VC modal, o fundo azul faz parte do VC que apresentou o modal. Há esse comportamento problemático quando o reconhecedor de gesto de panorâmica está iniciando que não consigo consertar.
Knolraap

1
Olá, @Knolraap. Talvez olhe para o valor de self.view.frame.originbefore you call sliceViewVerticallyTopela primeira vez: parece que o deslocamento que vemos é o mesmo que a altura da barra de status, então talvez sua origem inicial não seja 0?
agirault

1
É melhor usar slideViewVerticallyTocomo uma função aninhada em onPan.
Nik Kov

16

criou uma demonstração para arrastar interativamente para baixo para dispensar o controlador de visualização como o modo de descoberta do snapchat. Verifique este github para ver o projeto de amostra.

insira a descrição da imagem aqui


2
Ótimo, mas está realmente desatualizado. Alguém conhece outro projeto de amostra como este?
aluno de

14

Swift 4.x, usando Pangesture

Maneira simples

Vertical

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)
    }
}

Função auxiliar

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)
    }

Jeito difícil

Consulte isto -> https://github.com/satishVekariya/DraggableViewController


1
Tentei usar seu código. mas quero fazer uma pequena mudança onde minha subvisualização está na parte inferior e quando o usuário arrasta a visão, a altura da subvisualização também deve aumentar em relação à posição tocada. Observação: - o evento de gesto foi colocado em
subvisualização

Claro, você consegue
SPatel

Oi @SPatel. Alguma ideia de como modificar este código para usar para arrastar no lado esquerdo do eixo x, ou seja, movimentos negativos ao longo do eixo x?
Nikhil Pandey

1
@SPatel Você também precisa alterar o título da resposta, pois a vertical mostra os movimentos do eixo x e a horizontal mostra os movimentos do eixo y.
Nikhil Pandey

1
Lembre-se de definir modalPresentationStyle = UIModalPresentationOverFullScreenpara evitar a tela traseira atrás do view.
tounaobun

13

Eu descobri uma maneira super simples de fazer isso. Basta colocar o seguinte código em seu controlador de visualização:

Swift 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
Obrigado, funciona perfeitamente! Basta soltar um Pan Gesture Recogniser para a visualização no construtor de interface e conectar com o @IBAction acima.
balazs630

Funciona no Swift 5 também. Basta seguir as instruções fornecidas por @ balazs630.
LondonGuy

Acho que essa é a melhor maneira.
Enkha

@Alex Shubin, Como descartar ao arrastar de ViewController para TabbarController?
Vadlapalli Masthan

11

Atualiza massivamente o repo para Swift 4 .

Para o Swift 3 , criei o seguinte para apresentar um UIViewControllerda direita para a esquerda e descartá-lo com o gesto de panorâmica. Eu carreguei isso como um repositório GitHub .

insira a descrição da imagem aqui

DismissOnPanGesture.swift Arquivo:

//  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
    }
}

Fácil uso:

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
Simplesmente fantástico! Obrigado por compartilhar.
Sharad Chauhan,

Olá, Como posso apresentar usando o Pan Gesture Recognizer, por favor me ajude, obrigado
Muralikrishna

Estou começando um canal no youtube com tutoriais agora. Posso criar um episódio para cobrir esse problema para iOS 13+ / Swift 5
David Seek

6

O que você está descrevendo é uma animação de transição personalizada interativa . Você está personalizando a animação e o gesto de condução de uma transição, ou seja, a dispensa (ou não) de um controlador de visualização apresentado. A maneira mais fácil de implementá-lo é combinando um UIPanGestureRecognizer com um UIPercentDrivenInteractiveTransition.

Meu livro explica como fazer isso e postei exemplos (do livro). Este exemplo particular é uma situação diferente - a transição é lateral, não para baixo, e é para um controlador de barra de guias, não um controlador apresentado - mas a ideia básica é exatamente a mesma:

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

Se você baixar esse projeto e executá-lo, verá que o que está acontecendo é exatamente o que você está descrevendo, exceto que é lateral: se o arrasto for mais da metade, fazemos a transição, mas se não, cancelamos e voltamos a Lugar, colocar.


3
404 Página Não Encontrada.
trapper

6

Apenas dispensa vertical

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

Eu criei uma extensão fácil de usar.

Apenas inerente seu UIViewController com InteractiveViewController e você está pronto para InteractiveViewController

chame o método showInteractive () de seu controlador para mostrar como interativo.

insira a descrição da imagem aqui


4

Em Objective C: Aqui está o código

noviewDidLoad

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];
}

como controlar o tempo para deslizar para baixo antes de ser dispensado?
TonyTony de

2

Aqui está uma extensão que fiz com base na resposta de @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
                })
            }
        }
    }

}

USO

Dentro de seu controlador de visualização, você deseja deslizar:

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

e será dispensável deslizando do lado esquerdo do controlador de visualização, como um controlador de navegação.


Ei, usei seu código e ele funciona perfeitamente para deslizar para a direita, mas quero descartar quando deslizar para baixo. Como posso fazer isso? Por favor ajude!
Khush

2

Esta é minha classe simples para arrastar ViewController do eixo . Acabei de herdar sua classe de DraggableViewController.

MyCustomClass: DraggableViewController

Funciona apenas para o ViewController apresentado.

// 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

Para aqueles que realmente querem mergulhar um pouco mais fundo na transição UIViewController personalizada, eu recomendo este ótimo tutorial de raywenderlich.com .

O projeto de amostra final original contém bug. Então, eu corrigi e carreguei no repositório Github . O proj está em Swift 5, portanto você pode executá-lo e reproduzi-lo facilmente.

Aqui está uma prévia:

E é interativo também!

Feliz hackeamento!


0

Você pode usar um UIPanGestureRecognizer para detectar o arrasto do usuário e mover a visualização modal com ele. Se a posição final estiver longe o suficiente, a vista pode ser descartada ou animada de volta à sua posição original.

Confira esta resposta para obter mais informações sobre como implementar algo assim.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.