Você também está chateado com pop-ups em aplicativos? Neste artigo, mostrarei como ocultar e mostrar interativamente pop-ups, tornar a animação interrompível e não enfurecer meus clientes.

Em um artigo anterior, observei como você pode animar a exibição de um novo controlador.
Decidimos que o viewController
pode mostrar e ocultar animadamente:

Agora vamos ensiná-lo a responder ao gesto de ocultação.
Transição interativa
Adicione um gesto próximo
Para ensinar o controlador a fechar interativamente, você precisa adicionar um gesto e processá-lo. Todo o trabalho estará na classe TransitionDriver
:
class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? }
Você pode anexar um manipulador no local do DimmPresentationController, dentro de PanelTransition:
private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController }
Ao mesmo tempo, você precisa indicar que a ocultação se tornou gerenciável (já fizemos isso no último artigo):
Segure o gesto
Vamos começar com o gesto de fechamento: se você arrastar o painel para baixo, a animação de fechamento começará e o movimento do dedo afetará o grau de fechamento.
UIPercentDrivenInteractiveTransition
permite capturar a animação de transição e controlá-la manualmente. Possui update
, finish
, métodos de cancel
. É conveniente fazer o processamento de gestos em sua subclasse.
Processamento de gestos
private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause()
.begin
Inicie a desativação da maneira mais comum. Salvamos o link para o controlador no método link(to:)
.changed
Conte o incremento e passe para o método de update
. O valor aceito pode variar de 0 a 1; portanto, controlaremos o grau de conclusão da animação a partir do método interactionControllerForDismissal(using:)
. Os cálculos foram realizados na extensão do gesto, para que o código fique mais limpo.
Cálculos de gestos private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } }
Os cálculos são baseados em maxTranslation
, calculamos como a altura do controlador exibido:
var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 }
.end
Nós olhamos para a totalidade do gesto. Regra de conclusão: se mais da metade tiver mudado, feche. Nesse caso, o deslocamento deve ser considerado não apenas pela coordenada atual, mas também pela velocity
. Portanto, entendemos a intenção do usuário: ele pode não terminar no meio, mas deslizar muito para baixo. Ou vice-versa: retire, mas deslize para cima para retornar.
Cálculos ProjectedLocation private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat {
.cancelled
- acontecerá se você bloquear a tela do telefone ou se ligar. Você pode manipulá-lo como um bloco .ended
ou cancelar uma ação.
.failed
- acontecerá se o gesto for cancelado por outro gesto. Assim, por exemplo, um gesto de arrastar pode cancelar um gesto de toque.
.possible
- o estado inicial do gesto, geralmente não requer muito trabalho.
Agora o painel também pode ser fechado com um toque, mas o botão de dismiss
foi dismiss
. Isso aconteceu porque há uma propriedade wantsInteractiveStart
no TransitionDriver
; por padrão, é true
. Isso é normal para um furto, mas bloqueia a dismiss
usual.
Vamos decompor o comportamento com base no estado do gesto. Se o gesto foi iniciado, esse é um fechamento interativo e, se não foi iniciado, o usual:
override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } }
Agora o usuário pode controlar o esconderijo:

Transição de interrupção
Suponha que começamos a fechar nosso cartão, mas mudamos de idéia e queremos retornar. É simples: no estado .began
, chamamos pause()
para parar.
Mas você precisa separar dois cenários:
- quando começamos a nos esconder do gesto;
- quando interrompemos o atual.
Para fazer isso, depois de parar, verifique percentComplete:
se for 0, começamos a fechar o cartão manualmente, além de precisarmos chamar de dismiss
. Se não for 0, então a ocultação já começou, basta parar a animação:
case .began: pause()
Pressiono o botão e deslizo imediatamente para cima para cancelar a ocultação:

Pare de exibir o controlador
A situação inversa: o cartão começou a aparecer, mas não precisamos dele. Pegamos e enviamos deslizando de volta. Você pode interromper a animação da tela do controlador nas mesmas etapas.
Retorne o driver como um controlador de vídeo interativo:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver }
Processe o gesto, mas com valores de polarização reversa e integridade:
private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } }
Para separar show e hide, digitei enum com a direção atual da animação:
enum TransitionDirection { case present, dismiss }
A propriedade é armazenada no TransitionDriver
e afeta qual manipulador de gestos será usado:
var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } }
Também afeta o wantsInteractiveStart
. Como não planejamos mostrar o controlador com um gesto, retornamos false
para .present
:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } }
Bem, resta mudar a direção do gesto quando o controlador foi totalmente exibido. O melhor lugar é no PresentationController
:
override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } }
É possível sem enum?Parece que podemos confiar nas propriedades do controlador isBeingPresented
e isBeingDismissed
. Mas eles mostram apenas o processo, e também precisamos de direções possíveis: no início do fechamento interativo, ambos os valores serão false
e já precisamos saber que essa é a direção do fechamento. Isso pode ser resolvido por condições adicionais para verificar a hierarquia dos controladores, mas a atribuição explícita via enum
parece ser uma solução mais simples.
Agora você pode interromper a animação do show. Pressiono o botão e deslizo imediatamente para baixo:

Mostrar por gesto
Se você estiver criando um menu de hambúrguer para um aplicativo, provavelmente desejará mostrá-lo por gesto. Isso funciona como um esconderijo interativo, mas em um gesto, em vez de dismiss
chamamos de present
.
Vamos começar do fim. Em handlePresentation(recognizer:)
mostre o controlador:
case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) }
Vamos mostrar interativamente:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … }
Para que o código funcione, não há links suficientes para o presentingController
e o presentedController
. Nós os passaremos ao criar o gesto, adicione o UIScreenEdgePanGestureRecognizer
:
func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController
Você pode transferir controladores ao criar PanelTransition
:
class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() }
Resta criar o PanelTransition
:
- Vamos criar um controlador
child
no viewDidLoad
, pois podemos precisar de um controlador a qualquer momento. - Crie
PanelTransition
. Em seu construtor, o gesto é vinculado ao controlador. - Coloque o transitioningDelegate para o controlador filho.
Para fins de treinamento, deslizo de baixo para baixo, mas isso entra em conflito com o fechamento do aplicativo no iPhone X e no centro de controle. O uso de preferredScreenEdgesDeferringSystemGestures
desabilitou o furto do sistema a partir de baixo.
class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController()
Após a alteração, ocorreu um problema: após o primeiro fechamento do painel, ele permanece para sempre no status de TransitionDirection.dismiss
. Defina o status correto depois de ocultar o controlador no PresentationController
:
override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } }
O código de exibição interativo pode ser visualizado em um thread separado . É assim:

Conclusão
Como resultado, podemos mostrar o controlador com animação interrompida, e o usuário tem controle sobre o que está acontecendo na tela. Isso é muito melhor, porque a animação não bloqueia mais a interface, pode ser cancelada ou até acelerada.
Um exemplo pode ser visto no github.
Inscreva- se no canal Dodo Pizza Mobile.