Não apareça! Transições interrompíveis no iOS

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


 // PanelTransition.swift func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

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() //   percentComplete   0 let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } case .changed: update(percentComplete + r.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } 

.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 { // Velocity value func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat { // Magic formula from WWDC let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000 return self * multiplier } } extension CGPoint { static func +(left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y) } } 

.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() // Pause allows to detect percentComplete if percentComplete == 0 { presentedController?.dismiss(animated: true) } 

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 //    panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController.view.addGestureRecognizer(panRecognizer!) //    screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePresentation(recognizer:))) screenEdgePanRecognizer!.edges = .bottom presentingController.view.addGestureRecognizer(screenEdgePanRecognizer!) } 

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 :


  1. Vamos criar um controlador child no viewDidLoad , pois podemos precisar de um controlador a qualquer momento.
  2. Crie PanelTransition . Em seu construtor, o gesto é vinculado ao controlador.
  3. Coloque o transitioningDelegate para o controlador filho.
  4. 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() // 1 transition = PanelTransition(presented: child, presenting: self) // 2 // Setup the child child.modalPresentationStyle = .custom child.transitioningDelegate = transition // 3 } override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return .bottom // 4 } } 

    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.

Source: https://habr.com/ru/post/pt465073/


All Articles