Como alterar as animações Push e Pop em um aplicativo baseado em navegação


221

Eu tenho um aplicativo baseado em navegação e quero alterar a animação das animações push e pop. Como eu faria isso?

Editar 2018

Existem muitas respostas para essa pergunta e já faz um bom tempo que eu escolhi a resposta para o que considero mais relevante agora. Se houver alguém que pense de outra forma, por favor me avise nos comentários


25
A partir do iOS 7, existe uma API oficial para isso; consulte Suporte personalizado à animação de transição de UINavigationControllerDelegate . Há também um vídeo da WWDC 2013 sobre isso.
Jesse Rusak 23/09

Eu adicionei uma resposta (abaixo) para fazer isso no Swift - me deparei com essa pergunta perguntando sobre implementações do Swift, então pensei em concordar com a minha implementação subsequente.
djbp

1
Para um tutorial muito bom com a API oficial (iOS 7 ou superior), consulte: bradbambara.wordpress.com/2014/04/11/…
nikolovski

1
@JesseRusak atualizou o link para WWDC 2013 Vídeo: developer.apple.com/videos/play/wwdc2013-218
Wojciech Rutkowski

1
Mudou minha resposta aceita pessoal n galões. Espero que isto ajude! GLHF
Jab

Respostas:


35

Como alterar as animações Push e Pop em um aplicativo baseado em navegação ...

Para 2019, a "resposta final!"

Preâmbulo:

Digamos que você seja novo no desenvolvimento do iOS. Confusamente, a Apple fornece duas transições que podem ser usadas facilmente. São eles: "crossfade" e "flip".

Mas é claro que "crossfade" e "flip" são inúteis. Eles nunca são usados. Ninguém sabe por que a Apple forneceu essas duas transições inúteis!

Assim:

Digamos que você queira fazer uma transição comum e comum, como "slide". Nesse caso, você tem que fazer uma quantidade enorme de trabalho! .

Esse trabalho é explicado neste post.

Só para repetir:

Surpreendentemente: com o iOS, se você deseja as transições diárias mais simples e comuns (como um slide comum), é necessário todo o trabalho de implementar uma transição personalizada completa .

Veja como fazê-lo ...

1. Você precisa de um costume UIViewControllerAnimatedTransitioning

  1. Você precisa de um bool de sua preferência popStyle. (Ele está aparecendo ou aparecendo?)

  2. Você deve incluir transitionDuration(trivial) e a chamada principal,animateTransition

  3. De fato, você deve escrever duas rotinas diferentes para dentro animateTransition. Um para o empurrão e outro para o pop. Provavelmente nomeie-os animatePushe animatePop. No interior animateTransition, basta ramificar popStylepara as duas rotinas

  4. O exemplo abaixo faz uma simples mudança / remoção

  5. Em suas animatePushe animatePoprotinas. Você deve obter o "from view" e o "to view". (Como fazer isso, é mostrado no exemplo de código.)

  6. e você deve addSubview acessar a nova visualização "para".

  7. e você deve ligar completeTransitionno final do seu anime

Assim ..

  class SimpleOver: NSObject, UIViewControllerAnimatedTransitioning {
        
        var popStyle: Bool = false
        
        func transitionDuration(
            using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.20
        }
        
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            
            if popStyle {
                
                animatePop(using: transitionContext)
                return
            }
            
            let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
            let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
            
            let f = transitionContext.finalFrame(for: tz)
            
            let fOff = f.offsetBy(dx: f.width, dy: 55)
            tz.view.frame = fOff
            
            transitionContext.containerView.insertSubview(tz.view, aboveSubview: fz.view)
            
            UIView.animate(
                withDuration: transitionDuration(using: transitionContext),
                animations: {
                    tz.view.frame = f
            }, completion: {_ in 
                    transitionContext.completeTransition(true)
            })
        }
        
        func animatePop(using transitionContext: UIViewControllerContextTransitioning) {
            
            let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
            let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
            
            let f = transitionContext.initialFrame(for: fz)
            let fOffPop = f.offsetBy(dx: f.width, dy: 55)
            
            transitionContext.containerView.insertSubview(tz.view, belowSubview: fz.view)
            
            UIView.animate(
                withDuration: transitionDuration(using: transitionContext),
                animations: {
                    fz.view.frame = fOffPop
            }, completion: {_ in 
                    transitionContext.completeTransition(true)
            })
        }
    }

E depois ...

2. Use-o no seu controlador de exibição.

Nota: estranhamente, você só precisa fazer isso no "primeiro" controlador de exibição. (Aquele que está "embaixo".)

Com o que você coloca no topo , não faça nada . Fácil.

Então sua classe ...

class SomeScreen: UIViewController {
}

torna-se...

class FrontScreen: UIViewController,
        UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
    
    let simpleOver = SimpleOver()
    

    override func viewDidLoad() {
        
        super.viewDidLoad()
        navigationController?.delegate = self
    }

    func navigationController(
        _ navigationController: UINavigationController,
        animationControllerFor operation: UINavigationControllerOperation,
        from fromVC: UIViewController,
        to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        simpleOver.popStyle = (operation == .pop)
        return simpleOver
    }
}

É isso aí.

Empurre e pop exatamente como normal, sem alterações. Para impulsionar ...

let n = UIStoryboard(name: "nextScreenStoryboardName", bundle: nil)
          .instantiateViewController(withIdentifier: "nextScreenStoryboardID")
          as! NextScreen
navigationController?.pushViewController(n, animated: true)

e para abri-lo, você pode, se quiser, fazer isso na próxima tela:

class NextScreen: TotallyOrdinaryUIViewController {
    
    @IBAction func userClickedBackOrDismissOrSomethingLikeThat() {
        
        navigationController?.popViewController(animated: true)
    }
}

Ufa.


3. Aproveite também as outras respostas nesta página que explicam como substituir o AnimatedTransitioning

Vá até @AlanZeino e @elias responda para mais discussões sobre como fazer AnimatedTransitioningnos aplicativos para iOS hoje em dia!


Excelente! Se eu quiser que o gesto de deslize para trás da navegação também suporte o mesmo AnimatedTransitioning. Qualquer ideia?
sam chi wen

obrigado @samchiwen - na verdade, é exatamente o que são animatePushe animatePopsão .. as duas direções diferentes!
Fattie 26/08/19

268

Eu fiz o seguinte e funciona bem .. e é simples e fácil de entender ..

CATransition* transition = [CATransition animation];
transition.duration = 0.5;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade; //kCATransitionMoveIn; //, kCATransitionPush, kCATransitionReveal, kCATransitionFade
//transition.subtype = kCATransitionFromTop; //kCATransitionFromLeft, kCATransitionFromRight, kCATransitionFromTop, kCATransitionFromBottom
[self.navigationController.view.layer addAnimation:transition forKey:nil];
[[self navigationController] popViewControllerAnimated:NO];

E a mesma coisa para empurrar ..


Versão Swift 3.0:

let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionFade
self.navigationController?.view.layer.add(transition, forKey: nil)
_ = self.navigationController?.popToRootViewController(animated: false)

34
+1, essa é realmente a solução mais sensata. Apenas uma pequena nota para futuros visitantes: a Animated:NOpeça é vital. Se YESaprovada, as animações se misturam e causam efeitos engraçados.
DarkDust 23/02

12
Melhor solução até agora .. E para os novatos, não se esqueça de incluir QuartCore (#import <QuartzCore / QuartzCore.h>)
nomann

4
O único problema que tenho com esta solução é que o viewDidAppear do controlador de exibição enviado é chamado imediatamente depois que o envio sem animação. Existe uma maneira de contornar isso?
Pedro Mancheno

9
Meu problema com este código é que cada exibição parece piscar em cinza ou branco à medida que deslizam dentro ou fora.
22413 Chris

1
verificado no iOS 7.1.2 e iOS 8.3 - este código está funcionando bem também trabalhar bem para o métodosetViewControllers:
proff

256

É assim que eu sempre consegui concluir esta tarefa.

Para Push:

MainView *nextView=[[MainView alloc] init];
[UIView  beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.75];
[self.navigationController pushViewController:nextView animated:NO];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
[UIView commitAnimations];
[nextView release];

Para Pop:

[UIView  beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.75];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:NO];
[UIView commitAnimations];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelay:0.375];
[self.navigationController popViewControllerAnimated:NO];
[UIView commitAnimations];


Ainda recebo muito feedback disso, então vou atualizá-lo para usar blocos de animação, que é a maneira recomendada pela Apple de fazer animações de qualquer maneira.

Para Push:

MainView *nextView = [[MainView alloc] init];
[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [self.navigationController pushViewController:nextView animated:NO];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
                         }];

Para Pop:

[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:NO];
                         }];
[self.navigationController popViewControllerAnimated:NO];

3
Obrigado por isso. Mas o pop é feito automaticamente pelo UINavigationController. Como você substitui esse comportamento para poder chamar sua lógica pop personalizada?
Joshua Frank

1
@stuckj realmente funciona !!! você só precisa substituir superporself.navigationController
holierthanthou84

Existe alguma maneira de obter um slide da esquerda em vez do slide padrão da direita?
shim

O primeiro não mostra a nova exibição. O segundo não mostra animação. Resposta muito ruim! iOS 7.
Dmitry

2
por que você deu à UIViewControllersubclasse um nome sem a parte "ViewController". Este nome é mais apropriado para o UIView.
user2159978

29

para empurrar

CATransition *transition = [CATransition animation];
transition.duration = 0.3;
transition.type = kCATransitionFade;
//transition.subtype = kCATransitionFromTop;

[self.navigationController.view.layer addAnimation:transition forKey:kCATransition];
[self.navigationController pushViewController:ViewControllerYouWantToPush animated:NO];

para pop

CATransition *transition = [CATransition animation];
transition.duration = 0.3;
transition.type = kCATransitionFade;
//transition.subtype = kCATransitionFromTop;

[self.navigationController.view.layer addAnimation:transition forKey:kCATransition];
[self.navigationController popViewControllerAnimated:NO];

19

Resposta @Magnus, só então para Swift (2.0)

    let transition = CATransition()
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromTop
    self.navigationController!.view.layer.addAnimation(transition, forKey: nil)
    let writeView : WriteViewController = self.storyboard?.instantiateViewControllerWithIdentifier("WriteView") as! WriteViewController
    self.navigationController?.pushViewController(writeView, animated: false)

Algumas notas laterais:

Você pode fazer isso também com o Segue, basta implementar isso em prepareForSegueou shouldPerformSegueWithIdentifier. No entanto , isso também manterá a animação padrão. Para corrigir isso, você deve ir ao storyboard, clicar no Segue e desmarcar a caixa 'Animates'. Mas isso limitará seu aplicativo para o IOS 9.0 e superior (pelo menos quando o fiz no Xcode 7).

Ao fazer um segue, as duas últimas linhas devem ser substituídas por:

self.navigationController?.popViewControllerAnimated(false)

Mesmo que eu defina falso, isso meio que o ignora.


Como remover a cor preta em segundo plano no final da animação.
Madhu

Não funciona para obras de animação impulso controlador de exibição para controlador de vista Pop
Mukul Mais

16

Lembre-se de que em Swift , extensão são definitivamente seus amigos!

public extension UINavigationController {

    /**
     Pop current view controller to previous view controller.

     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func pop(transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.popViewControllerAnimated(false)
    }

    /**
     Push a new view controller on the view controllers's stack.

     - parameter vc:       view controller to push.
     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func push(viewController vc: UIViewController, transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.pushViewController(vc, animated: false)
    }

    private func addTransition(transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        let transition = CATransition()
        transition.duration = duration
        transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        transition.type = type
        self.view.layer.addAnimation(transition, forKey: nil)
    }

}

11

Usar chamadas privadas é uma péssima idéia, pois a Apple não aprova mais os aplicativos que fazem isso. Talvez você possa tentar o seguinte:

//Init Animation
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.50];


[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:self.navigationController.view cache:YES];

//Create ViewController
MyViewController *myVC = [[MyViewController alloc] initWith...];

[self.navigationController pushViewController:myVC animated:NO];
[myVC release];

//Start Animation
[UIView commitAnimations];

Apenas "metade" funciona - não vai resolver o problema mais difícil das animações pop.
Adam

Eu gosto mais desta solução e sim funciona. Usar métodos privados fará com que você seja rejeitado com certeza.
Benjamin Intal

@nicktmro, que é a chamada particular da API. Eu não notei nada.
31413 Franklin

@Franklin, houve uma discussão aqui há um tempo atrás, usando o -pushViewController:transition:forceImmediate:que seria uma má idéia.
Nicktmro 29/08

9

Como esse é o melhor resultado no Google, pensei em compartilhar o que considero a maneira mais sensata; que é usar a API de transição do iOS 7+. Eu implementei isso no iOS 10 com o Swift 3.

É bem simples combinar isso com como UINavigationController anima entre dois controladores de exibição, se você criar uma subclasse UINavigationControllere retornar uma instância de uma classe que esteja em conformidade com o UIViewControllerAnimatedTransitioningprotocolo.

Por exemplo, aqui está o meu UINavigationController subclasse:

class NavigationController: UINavigationController {
    init() {
        super.init(nibName: nil, bundle: nil)

        delegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension NavigationController: UINavigationControllerDelegate {

    public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return NavigationControllerAnimation(operation: operation)
    }

}

Você pode ver que eu defino o valor UINavigationControllerDelegateem si e, em uma extensão da minha subclasse, implemento o método em UINavigationControllerDelegateque permite retornar um controlador de animação personalizado (por exemplo, NavigationControllerAnimation). Este controlador de animação personalizado substituirá a animação de estoque para você.

Você provavelmente está se perguntando por que eu passo a operação para o NavigationControllerAnimation instância por meio de seu inicializador. Eu faço isso para que na NavigationControllerAnimationimplementação do UIViewControllerAnimatedTransitioningprotocolo eu saiba qual é a operação (ou seja, 'push' ou 'pop'). Isso ajuda a saber que tipo de animação devo fazer. Na maioria das vezes, você deseja executar uma animação diferente, dependendo da operação.

O resto é bastante padrão. Implemente as duas funções necessárias noUIViewControllerAnimatedTransitioning protocolo e anime da maneira que desejar:

class NavigationControllerAnimation: NSObject, UIViewControllerAnimatedTransitioning {

    let operation: UINavigationControllerOperation

    init(operation: UINavigationControllerOperation) {
        self.operation = operation

        super.init()
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.3
    }

    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { return }
        let containerView = transitionContext.containerView

        if operation == .push {
            // do your animation for push
        } else if operation == .pop {
            // do your animation for pop
        }
    }
}

É importante lembrar que, para cada tipo diferente de operação (por exemplo, 'push' ou 'pop'), os controladores de e para a visualização serão diferentes. Quando você estiver em uma operação de envio, o controlador para visualizar será o que está sendo enviado. Quando você está em uma operação pop, o controlador de visualização será aquele que está sendo transferido e o controlador de visualização será o que está sendo exibido.

Além disso, o tocontrolador de exibição deve ser adicionado como uma subvisão docontainerView no contexto de transição.

Quando sua animação terminar, você deve ligar transitionContext.completeTransition(true). Se você estiver fazendo uma transição interativa, será necessário retornar dinamicamente BoolacompleteTransition(didComplete: Bool) , dependendo se a transição estiver concluída no final da animação.

Finalmente ( leitura opcional ), você pode querer ver como eu fiz a transição em que estava trabalhando. Esse código é um pouco mais hacky e eu o escrevi rapidamente, então não diria que é um ótimo código de animação, mas ainda mostra como fazer a parte da animação.

A minha foi uma transição realmente simples; Eu queria imitar a mesma animação que o UINavigationController normalmente faz, mas, em vez da animação 'próxima página por cima', eu queria implementar uma animação 1: 1 do antigo controlador de exibição ao mesmo tempo que a nova exibição controlador aparece. Isso tem o efeito de fazer com que os dois controladores de exibição pareçam estar presos um ao outro.

Para a operação push, é necessário primeiro definir a toViewControllerorigem da vista da tela no eixo x fora da tela, adicionando-a como subvisão do containerView, animando-a na tela, definindo- origin.xa como zero. Ao mesmo tempo, animar a fromViewControllervista de fora definindo origin.x-a na tela:

toViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.size.width, dy: 0.0)

containerView.addSubview(toViewController.view)

UIView.animate(withDuration: transitionDuration(using: transitionContext),
               delay: 0,
               options: [ UIViewAnimationOptions.curveEaseOut ],
               animations: {
                toViewController.view.frame = containerView.bounds
                fromViewController.view.frame = containerView.bounds.offsetBy(dx: -containerView.frame.size.width, dy: 0)
},
               completion: { (finished) in
                transitionContext.completeTransition(true)
})

A operação pop é basicamente o inverso. Adicione a toViewControllercomo uma subvisão de containerViewe animar fromViewControllerpara a direita como você anima na toViewControllerda esquerda:

containerView.addSubview(toViewController.view)

UIView.animate(withDuration: transitionDuration(using: transitionContext),
               delay: 0,
               options: [ UIViewAnimationOptions.curveEaseOut ],
               animations: {
                fromViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.width, dy: 0)
                toViewController.view.frame = containerView.bounds
},
               completion: { (finished) in
                transitionContext.completeTransition(true)
})

Aqui está uma essência com todo o arquivo rápido:

https://gist.github.com/alanzeino/603293f9da5cd0b7f6b60dc20bc766be


Ótimo!. O que eu queria fazer era apenas animar na direção oposta. Examinei algumas outras soluções, mas todas exibem piscando na tela esquerda e direita. Parece que a animação implícita de alteração alfa não pode ser removida com eles. Somente esta solução corrigiu o problema.
beshio

Sim, esta é a única solução moderna correta. (Eu não me importo, mas é exatamente o mesmo que a solução que eu digitei abaixo :)!)
Fattie

@AlanZeino E se dentro do mesmo ViewController você precisar ter Animações diferentes para clicar em um botão diferente? Portanto, para o button1, você precisa de uma animação dissolvida, para o button2, você precisa da transição padrão.
jzeferino

7

Existem UINavigationControllerDelegate e UIViewControllerAnimatedTransitioning lá, você pode alterar a animação para o que quiser.

Por exemplo, isso é animação pop vertical para VC:

@objc class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
    return 0.5
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let containerView = transitionContext.containerView()
    let bounds = UIScreen.mainScreen().bounds
    containerView!.insertSubview(toViewController.view, belowSubview: fromViewController.view)
    toViewController.view.alpha = 0.5

    let finalFrameForVC = fromViewController.view.frame

    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
        fromViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.height)
        toViewController.view.alpha = 1.0
        }, completion: {
            finished in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })
}

}

E depois

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if operation == .Pop {
        return PopAnimator()
    }
    return nil;
}

Tutorial útil https://www.objc.io/issues/5-ios7/view-controller-transitions/


6

Com base na resposta atualizada para o swift 4jordanperry

Para empurrar UIViewController

let yourVC = self.storyboard?.instantiateViewController(withIdentifier: "yourViewController") as! yourViewController
    UIView.animate(withDuration: 0.75, animations: {() -> Void in
    UIView.setAnimationCurve(.easeInOut)
    self.navigationController?.pushViewController(terms, animated: true)
    UIView.setAnimationTransition(.flipFromRight, for: (self.navigationController?.view)!, cache: false)
})

Para Pop

UIView.animate(withDuration: 0.75, animations: {() -> Void in
    UIView.setAnimationCurve(.easeInOut)
    UIView.setAnimationTransition(.flipFromLeft, for: (self.navigationController?.view)!, cache: false)
})
navigationController?.popViewController(animated: false)

5

Aqui está como eu fiz o mesmo no Swift:

Para Push:

    UIView.animateWithDuration(0.75, animations: { () -> Void in
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        self.navigationController!.pushViewController(nextView, animated: false)
        UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromRight, forView: self.navigationController!.view!, cache: false)
    })

Para Pop:

Na verdade, fiz isso de maneira um pouco diferente para algumas das respostas acima - mas como sou novo no desenvolvimento do Swift, pode não estar certo. Eu substituí viewWillDisappear:animated:e adicionei o código pop lá:

    UIView.animateWithDuration(0.75, animations: { () -> Void in
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromLeft, forView: self.navigationController!.view, cache: false)
    })

    super.viewWillDisappear(animated)

5

Resposta de @Luca Davanzo no Swift 4.2

public extension UINavigationController {

    /**
     Pop current view controller to previous view controller.

     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func pop(transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.popViewController(animated: false)
    }

    /**
     Push a new view controller on the view controllers's stack.

     - parameter vc:       view controller to push.
     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func push(viewController vc: UIViewController, transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.pushViewController(vc, animated: false)
    }

    private func addTransition(transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        let transition = CATransition()
        transition.duration = duration
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        transition.type = type
        self.view.layer.add(transition, forKey: nil)
    }

}

4

Recentemente, eu estava tentando fazer algo semelhante. Decidi que não gostava da animação deslizante do UINavigationController, mas também não queria fazer as animações que o UIView oferece para você como curl ou algo assim. Eu queria fazer um cross fade entre as vistas quando empurro ou pop.

O problema envolve o fato de que a visualização está literalmente removendo ou exibindo uma sobre a parte superior da atual, para que o desbotamento não funcione. A solução que encontrei envolveu pegar minha nova visão e adicioná-la como uma sub-visão à visão superior atual na pilha do UIViewController. Eu o adiciono com um alfa de 0 e depois faço um crossfade. Quando a sequência de animação termina, empurro a vista para a pilha sem a animar. Volto ao antigo topView e limpo as coisas que eu havia mudado.

É um pouco mais complicado que isso, porque você tem os itens de navegação que precisa ajustar para fazer a transição parecer correta. Além disso, se você fizer uma rotação, precisará ajustar os tamanhos dos quadros à medida que adiciona as visualizações como subvisões para que elas apareçam corretamente na tela. Aqui está um pouco do código que eu usei. Subclassifiquei o UINavigationController e substitui os métodos push e pop.

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
      UIViewController *currentViewController = [self.viewControllers lastObject];
      //if we don't have a current controller, we just do a normal push
      if(currentViewController == nil)
      {
         [super pushViewController:viewController animated:animated];
         return;
      }
      //if no animation was requested, we can skip the cross fade
      if(!animation)
      {
         [super pushViewController:viewController animated:NO];
         return;
      }
      //start the cross fade.  This is a tricky thing.  We basically add the new view
//as a subview of the current view, and do a cross fade through alpha values.
//then we push the new view on the stack without animating it, so it seemlessly is there.
//Finally we remove the new view that was added as a subview to the current view.

viewController.view.alpha = 0.0;
//we need to hold onto this value, we'll be releasing it later
    NSString *title = [currentViewController.title retain];

//add the view as a subview of the current view
[currentViewController.view addSubview:viewController.view];
[currentViewController.view bringSubviewToFront:viewController.view];
UIBarButtonItem *rButtonItem = currentViewController.navigationItem.rightBarButtonItem;
UIBarButtonItem *lButtonItem = currentViewController.navigationItem.leftBarButtonItem;

NSArray *array = nil;

//if we have a right bar button, we need to add it to the array, if not, we will crash when we try and assign it
//so leave it out of the array we are creating to pass as the context.  I always have a left bar button, so I'm not checking to see if it is nil. Its a little sloppy, but you may want to be checking for the left BarButtonItem as well.
if(rButtonItem != nil)
    array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,rButtonItem,nil];
else {
    array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,nil];
}

//remove the right bar button for our transition
[currentViewController.navigationItem setRightBarButtonItem:nil animated:YES];
//remove the left bar button and create a backbarbutton looking item
//[currentViewController.navigationItem setLeftBarButtonItem:nil animated:NO];

//set the back button
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:title style:kButtonStyle target:self action:@selector(goBack)];
[currentViewController.navigationItem setLeftBarButtonItem:backButton animated:YES];
[viewController.navigationItem setLeftBarButtonItem:backButton animated:NO];
[backButton release];

[currentViewController setTitle:viewController.title];

[UIView beginAnimations:@"push view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePushDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[viewController.view setAlpha: 1.0];
[UIView commitAnimations];
}

-(void)animationForCrossFadePushDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{

UIViewController *c = [(NSArray*)context objectAtIndex:0];
UIViewController *n = [(NSArray*)context objectAtIndex:1];
NSString *title     = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:3];
UIBarButtonItem *r = nil;
//not all views have a right bar button, if we look for it and it isn't in the context,
//we'll crash out and not complete the method, but the program won't crash.
//So, we need to check if it is there and skip it if it isn't.
if([(NSArray *)context count] == 5)
    r = [(NSArray *)context objectAtIndex:4];

//Take the new view away from being a subview of the current view so when we go back to it
//it won't be there anymore.
[[[c.view subviews] lastObject] removeFromSuperview];
[c setTitle:title];
[title release];
//set the search button
[c.navigationItem setLeftBarButtonItem:l animated:NO];
//set the next button
if(r != nil)
    [c.navigationItem setRightBarButtonItem:r animated:NO];


[super pushViewController:n animated:NO];

 }

Como mencionei no código, sempre tenho um item de botão de barra esquerdo, portanto não verifico se é nulo antes de colocá-lo na matriz que passo como contexto para o representante da animação. Se você fizer isso, convém fazer essa verificação.

O problema que encontrei foi que, se você travar no método delegado, ele não travará o programa. Isso apenas impede o delegado de concluir, mas você não recebe nenhum tipo de aviso.
Então, como eu estava fazendo minha limpeza nessa rotina de delegados, estava causando um comportamento visual estranho, pois não estava terminando a limpeza.

O botão voltar criado por mim chama um método "goBack", e esse método chama apenas a rotina pop.

-(void)goBack
{ 
     [self popViewControllerAnimated:YES];
}

Além disso, aqui está minha rotina pop.

-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    //get the count for the number of viewControllers on the stack
int viewCount = [[self viewControllers] count];
//get the top view controller on the stack
UIViewController *topViewController = [self.viewControllers objectAtIndex:viewCount - 1];
//get the next viewController after the top one (this will be the new top one)
UIViewController *newTopViewController = [self.viewControllers objectAtIndex:viewCount - 2];

//if no animation was requested, we can skip the cross fade
if(!animated)
{
    [super popViewControllerAnimated:NO];
            return topViewController;
}



//start of the cross fade pop.  A bit tricky.  We need to add the new top controller
//as a subview of the curent view controler with an alpha of 0.  We then do a cross fade.
//After that we pop the view controller off the stack without animating it.
//Then the cleanup happens: if the view that was popped is not released, then we
//need to remove the subview we added and change some titles back.
newTopViewController.view.alpha = 0.0;
[topViewController.view addSubview:newTopViewController.view];
[topViewController.view bringSubviewToFront:newTopViewController.view];
NSString *title = [topViewController.title retain];
UIBarButtonItem *lButtonItem = topViewController.navigationItem.leftBarButtonItem;
UIBarButtonItem *rButtonItem = topViewController.navigationItem.rightBarButtonItem;

//set the new buttons on top of the current controller from the new top controller
if(newTopViewController.navigationItem.leftBarButtonItem != nil)
{
    [topViewController.navigationItem setLeftBarButtonItem:newTopViewController.navigationItem.leftBarButtonItem animated:YES];
}
if(newTopViewController.navigationItem.rightBarButtonItem != nil)
{
    [topViewController.navigationItem setRightBarButtonItem:newTopViewController.navigationItem.rightBarButtonItem animated:YES];
}

[topViewController setTitle:newTopViewController.title];
//[topViewController.navigationItem.leftBarButtonItem setTitle:newTopViewController.navigationItem.leftBarButtonItem.title];

NSArray *array = nil;
if(rButtonItem != nil)
    array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,rButtonItem,nil];
else {
    array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,nil];
}


[UIView beginAnimations:@"pop view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePopDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[newTopViewController.view setAlpha: 1.0];
[UIView commitAnimations];
return topViewController;

 }

 -(void)animationForCrossFadePopDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
 {

UIViewController *c = [(NSArray *)context objectAtIndex:0];
//UIViewController *n = [(NSArray *)context objectAtIndex:1];
NSString *title = [(NSArray *)context objectAtIndex:1];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *r = nil;



//Not all views have a right bar button.  If we look for one that isn't there
// we'll crash out and not complete this method, but the program will continue.
//So we need to check if it is therea nd skip it if it isn't.
if([(NSArray *)context count] == 4)
    r = [(NSArray *)context objectAtIndex:3];

//pop the current view from the stack without animation
[super popViewControllerAnimated:NO];

//if what was the current veiw controller is not nil, then lets correct the changes
//we made to it.
if(c != nil)
{
    //remove the subview we added for the transition
    [[c.view.subviews lastObject] removeFromSuperview];
    //reset the title we changed
    c.title = title;
    [title release];
    //replace the left bar button that we changed
    [c.navigationItem setLeftBarButtonItem:l animated:NO];
    //if we were passed a right bar button item, replace that one as well
    if(r != nil)
        [c.navigationItem setRightBarButtonItem:r animated:NO];
    else {
        [c.navigationItem setRightBarButtonItem:nil animated:NO];
    }


 }
}

É praticamente isso. Você precisará de algum código adicional se desejar implementar rotações. Você precisará definir o tamanho do quadro de suas vistas que você adiciona como subvisões antes de mostrá-las. Caso contrário, você terá problemas em que a orientação é paisagem, mas a última vez que viu a vista anterior era em retrato. Então, você o adiciona como uma subvisualização e a desvanece, mas ele aparece como retrato; depois, quando exibimos sem animação, a mesma visualização, mas a que está na pilha, agora é paisagem. A coisa toda parece um pouco estranha. A implementação de rotação de todos é um pouco diferente, então não incluí meu código aqui.

Espero que ajude algumas pessoas. Procurei em todo o mundo algo assim e não encontrei nada. Eu não acho que essa seja a resposta perfeita, mas está funcionando muito bem para mim neste momento.


Embora admirável, essa honestamente não é a solução agora 7 anos depois!
Fattie

Você está certo. Esta resposta foi de 2011. Funcionou naquela época, mas as coisas mudaram muito desde então. =)
georryan

4

Agora você pode usar UIView.transition. Note isso animated:false. Isso funciona com qualquer opção de transição, pop, push ou pilha substituída.

if let nav = self.navigationController
{
    UIView.transition(with:nav.view, duration:0.3, options:.transitionCrossDissolve, animations: {
        _ = nav.popViewController(animated:false)
    }, completion:nil)
}

1
@Fattie, este método particular só funciona com qualquer uma das animações padrão como flips e cachos listados na developer.apple.com/documentation/uikit/uiviewanimationoptions
Peter DeWeese

3

Usando a resposta da iJordan como inspiração, por que não simplesmente criar uma categoria no UINavigationController para usar em todo o aplicativo em vez de copiar / colar esse código de animação em todo o lugar?

UINavigationController + Animation.h

@interface UINavigationController (Animation)

- (void) pushViewControllerWithFlip:(UIViewController*) controller;

- (void) popViewControllerWithFlip;

@end

UINavigationController + Animation.m

@implementation UINavigationController (Animation)

- (void) pushViewControllerWithFlip:(UIViewController *) controller
{
    [UIView animateWithDuration:0.50
                     animations:^{
                         [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                         [self pushViewController:controller animated:NO];
                         [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:NO];
                     }];
}

- (void) popViewControllerWithFlip
{
    [UIView animateWithDuration:0.5
                     animations:^{
                         [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                         [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:NO];
                     }];

    [self popViewControllerAnimated:NO];
}

@end

Em seguida, basta importar o arquivo UINavigationController + Animation.h e chamá-lo normalmente:

[self.navigationController pushViewControllerWithFlip:[[NewViewController alloc] init]];

[self.navigationController popViewControllerWithFlip];

Inteligente. Mas por que não adicionar métodos push / pop que usam um argumento de UIViewAnimationTransition em vez de hardcode para flipFromRight?
Jef

@ Se esses são métodos de conveniência - dessa maneira, o implementador não precisa se lembrar de qual valor UIViewAnimationTransition deve ser transferido para cada tipo de animação específico, eles apenas chamam o método com o nome "inglês" do que desejam realizar.
DiscDev 7/03/16

@ Jeff também, sua sugestão é definitivamente válida - se eu ainda estivesse usando o objetivo-c e precisasse oferecer suporte a muitos estilos de transição (definitivamente não recomendado, pois muitos estilos de transição diferentes confundirão os usuários) eu teria um método que usa o tipo UIViewAnimationTransition, vários métodos de conveniência para facilitar o desenvolvimento.
precisa saber é o seguinte

3

É muito simples

self.navigationController?.view.semanticContentAttribute = .forceRightToLeft

Bem-vindo ao StackOverflow: se você postar amostras de código, XML ou dados, realce essas linhas no editor de texto e clique no botão "amostras de código" ({}) na barra de ferramentas do editor ou usando Ctrl + K no teclado para formatar de forma adequada e sintaxe destacá-lo!
WhatsThePoint

2

Dê uma olhada no ADTransitionController , uma substituição do UINavigationController com animações de transição personalizadas (sua API corresponde à API do UINavigationController) que criamos no Applidium.

Você pode usar diferentes animações predefinidas para ações push e pop , como deslizar , Fade , Cube , Carrousel , Zoom e assim por diante.


2

Embora todas as respostas aqui sejam ótimas e a maioria funcione muito bem, existe um método um pouco mais simples que obtém o mesmo efeito ...

Para Push:

  NextViewController *nextViewController = [[NextViewController alloc] init];

  // Shift the view to take the status bar into account 
  CGRect frame = nextViewController.view.frame;
  frame.origin.y -= 20;
  frame.size.height += 20;
  nextViewController.view.frame = frame;

  [UIView transitionFromView:self.navigationController.topViewController.view toView:nextViewController.view duration:0.5 options:UIViewAnimationOptionTransitionFlipFromRight completion:^(BOOL finished) {
    [self.navigationController pushViewController:nextViewController animated:NO];
  }];

Para Pop:

  int numViewControllers = self.navigationController.viewControllers.count;
  UIView *nextView = [[self.navigationController.viewControllers objectAtIndex:numViewControllers - 2] view];

  [UIView transitionFromView:self.navigationController.topViewController.view toView:nextView duration:0.5 options:UIViewAnimationOptionTransitionFlipFromLeft completion:^(BOOL finished) {
    [self.navigationController popViewControllerAnimated:NO];
  }];}

Isso trava ao aparecer no controlador de visualização raiz.
Abdullah Umer

1

Não conheço nenhuma maneira de alterar publicamente a animação de transição.

Se o botão "voltar" não for necessário, você deve usar os controladores de exibição modal para fazer as transições "empurrar de baixo" / "virar" / "desaparecer" / (≥3,2) "curvatura da página".


No lado privado , o método -pushViewController:animated:chama o método não documentado -pushViewController:transition:forceImmediate:; portanto, se você deseja uma transição flip da esquerda para a direita, pode usar

[navCtrler pushViewController:ctrler transition:10 forceImmediate:NO];

Você não pode alterar a transição "pop" dessa maneira, no entanto.


1

Veja minha resposta a esta pergunta para uma maneira de fazê-lo em muito menos linhas de código. Esse método permite animar um pseudo- "Push" de um novo controlador de exibição da maneira que você quiser, e quando a animação é concluída, ele configura o Navigation Controller como se você tivesse usado o método Push padrão. Meu exemplo permite animar um slide-in da esquerda ou da direita. Código repetido aqui por conveniência:

-(void) showVC:(UIViewController *) nextVC rightToLeft:(BOOL) rightToLeft {
    [self addChildViewController:neighbor];
    CGRect offscreenFrame = self.view.frame;
    if(rightToLeft) {
        offscreenFrame.origin.x = offscreenFrame.size.width * -1.0;
    } else if(direction == MyClimbDirectionRight) {
        offscreenFrame.origin.x = offscreenFrame.size.width;
    }
    [[neighbor view] setFrame:offscreenFrame];
    [self.view addSubview:[neighbor view]];
    [neighbor didMoveToParentViewController:self];
    [UIView animateWithDuration:0.5 animations:^{
        [[neighbor view] setFrame:self.view.frame];
    } completion:^(BOOL finished){
        [neighbor willMoveToParentViewController:nil];
        [neighbor.view removeFromSuperview];
        [neighbor removeFromParentViewController];
        [[self navigationController] pushViewController:neighbor animated:NO];
        NSMutableArray *newStack = [[[self navigationController] viewControllers] mutableCopy];
        [newStack removeObjectAtIndex:1]; //self, just below top
        [[self navigationController] setViewControllers:newStack];
    }];
}

0

No aplicativo de amostra, confira esta variação. https://github.com/mpospese/MPFoldTransition/

#pragma mark - UINavigationController(MPFoldTransition)

@implementation UINavigationController(MPFoldTransition)

//- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
- (void)pushViewController:(UIViewController *)viewController foldStyle:(MPFoldStyle)style
{
    [MPFoldTransition transitionFromViewController:[self visibleViewController] 
                                  toViewController:viewController 
                                          duration:[MPFoldTransition defaultDuration]  
                                             style:style 
                                        completion:^(BOOL finished) {
                                            [self pushViewController:viewController animated:NO];
                                        }
     ];
}

- (UIViewController *)popViewControllerWithFoldStyle:(MPFoldStyle)style
{
    UIViewController *toController = [[self viewControllers] objectAtIndex:[[self viewControllers] count] - 2];

    [MPFoldTransition transitionFromViewController:[self visibleViewController] 
                                  toViewController:toController 
                                          duration:[MPFoldTransition defaultDuration] 
                                             style:style
                                        completion:^(BOOL finished) {
                                            [self popViewControllerAnimated:NO];
                                        }
     ];

    return toController;
}

0

Apenas use:

ViewController *viewController = [[ViewController alloc] init];

UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
navController.navigationBarHidden = YES;

[self presentViewController:navController animated:YES completion: nil];
[viewController release];
[navController release];

0

Perceber que essa é uma pergunta antiga. Eu ainda gostaria de postar esta resposta, pois tive alguns problemas ao exibir várias viewControllerscom as respostas propostas. Minha solução é subclassificar UINavigationControllere substituir todos os métodos pop e push.

FlippingNavigationController.h

@interface FlippingNavigationController : UINavigationController

@end

FlippingNavigationController.m:

#import "FlippingNavigationController.h"

#define FLIP_DURATION 0.5

@implementation FlippingNavigationController

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [UIView transitionWithView:self.view
                      duration:animated?FLIP_DURATION:0
                       options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionFlipFromRight
                    animations:^{ [super pushViewController:viewController
                                                   animated:NO]; }
                    completion:nil];
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    return [[self popToViewController:[self.viewControllers[self.viewControllers.count - 2]]
                             animated:animated] lastObject];
}

- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
{
    return [self popToViewController:[self.viewControllers firstObject]
                            animated:animated];
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    __block NSArray* viewControllers = nil;

    [UIView transitionWithView:self.view
                      duration:animated?FLIP_DURATION:0
                       options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionFlipFromLeft
                    animations:^{ viewControllers = [super popToViewController:viewController animated:NO]; }
                    completion:nil];

    return viewControllers;
}

@end

0

Eu sei que esse tópico é antigo, mas pensei em colocar meus dois centavos. Você não precisa criar uma animação personalizada, existe uma maneira simples (talvez hacky) de fazê-lo. Em vez de usar push, crie um novo controlador de navegação, torne o novo controlador de visualização o controlador de visualização raiz desse controlador nav e, em seguida, apresente o controlador nav a partir do controlador nav original. O presente é facilmente personalizável com vários estilos e não é necessário criar uma animação personalizada.

Por exemplo:

UIViewcontroller viewControllerYouWantToPush = UIViewController()
UINavigationController newNavController = UINavigationController(root: viewControllerYouWantToView)
newNavController.navBarHidden = YES;
self.navigationController.present(newNavController)

E você pode alterar o estilo de apresentação como quiser.


-1

Eu encontrei uma maneira levemente recursiva de fazer isso que funciona para meus propósitos. Eu tenho uma variável de instância BOOL que eu uso para bloquear a animação popping normal e substituir minha própria mensagem pop não animada. A variável é inicialmente definida como NÃO. Quando o botão Voltar é pressionado, o método delegado o define como SIM e envia uma nova mensagem pop não animada para a barra de navegação, chamando novamente o mesmo método delegado, desta vez com a variável definida como YES. Com a variável definida como YES, o método delegate a define como NO e retorna YES para permitir a ocorrência de pop não animado. Após o retorno da segunda chamada de delegado, terminamos novamente na primeira, onde NO é retornado, bloqueando o pop animado original! Na verdade, não é tão confuso quanto parece. Meu método shouldPopItem se parece com isso:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item 
{
    if ([[navigationBar items] indexOfObject:item] == 1) 
    {
        [expandedStack restack];    
    }

    if (!progPop) 
    {
        progPop = YES;
        [navBar popNavigationItemAnimated:NO];
        return NO;
    }
    else 
    {
        progPop = NO;
        return YES;
    }
}

Funciona para mim.

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.