Blok uzupełniania dla popViewController

115

Podczas odrzucania modalnego kontrolera widoku za pomocą dismissViewController, istnieje opcja zapewnienia bloku uzupełniania. Czy istnieje podobny odpowiednik popViewController?

Argument ukończenia jest całkiem przydatny. Na przykład mogę go użyć do wstrzymania usuwania wiersza z widoku tabeli, dopóki modal nie zniknie z ekranu, pozwalając użytkownikowi zobaczyć animację wiersza. Wracając z wypychanego kontrolera widoku, chciałbym mieć taką samą możliwość.

Próbowałem umieścić popViewControllerw UIViewbloku animacji, gdzie mam dostęp do bloku uzupełniania. Jednak powoduje to niepożądane efekty uboczne na wyskakującym widoku.

Jeśli nie ma takiej metody, jakie są obejścia?

Ben Packard
źródło
stackoverflow.com/a/33767837/2774520 myślę, że ten sposób jest najbardziej natywny
Oleksii Nezhyborets
3
W 2018 roku jest to bardzo proste i standardowe: stackoverflow.com/a/43017103/294884
Fattie

Odpowiedzi:

201

Wiem, że odpowiedź została przyjęta ponad dwa lata temu, jednak jest ona niepełna.

Nie ma sposobu, aby zrobić to, czego chcesz, po wyjęciu z pudełka

Jest to technicznie poprawne, ponieważ UINavigationControllerinterfejs API nie oferuje żadnych opcji. Jednak za pomocą struktury CoreAnimation można dodać blok uzupełniania do podstawowej animacji:

[CATransaction begin];
[CATransaction setCompletionBlock:^{
    // handle completion here
}];

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

Blok ukończenia zostanie wywołany zaraz po zakończeniu animacji popViewControllerAnimated:. Ta funkcjonalność jest dostępna od iOS 4.

Joris Kluivers
źródło
5
Umieściłem to w rozszerzeniu UINavigationController w Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur
1
Wydaje się, że nie działa dla mnie, kiedy wykonuję CompleteHandler na fragissViewController, prezentowany widok jest częścią hierarchii widoków. Kiedy robię to samo z CATransaction, otrzymuję ostrzeżenie, że widok nie jest częścią hierarchii widoków.
moger777
1
OK, wygląda na to, że pracujesz, jeśli odwrócisz blok rozpoczęcia i zakończenia. Przepraszam za głosowanie
negatywne,
7
Tak, wydawało się, że byłoby niesamowite, ale nie działa (przynajmniej na iOS 8). Blok uzupełniania jest wywoływany natychmiast. Prawdopodobnie z powodu połączenia głównych animacji z animacjami w stylu UIView.
utknąłem
5
TO NIE DZIAŁA
durazno
52

Dla wersji SWIFT na iOS9 - działa jak urok (nie był testowany dla wcześniejszych wersji). Na podstawie tej odpowiedzi

extension UINavigationController {    
    func pushViewController(viewController: UIViewController, animated: Bool, completion: () -> ()) {
        pushViewController(viewController, animated: animated)

        if let coordinator = transitionCoordinator() where animated {
            coordinator.animateAlongsideTransition(nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }

    func popViewController(animated: Bool, completion: () -> ()) {
        popViewControllerAnimated(animated)

        if let coordinator = transitionCoordinator() where animated {
            coordinator.animateAlongsideTransition(nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
HotJard
źródło
Nie będzie działać, jeśli nie jest animowany, należy wykonać zakończenie w następnym runloopie, aby zrobić to poprawnie.
rshev
@rshev dlaczego w następnym runloopie?
Ben Sinclair
@Andy z tego, co pamiętam, eksperymentowałem z tym, coś nie zostało jeszcze rozpowszechnione w tamtym momencie. Spróbuj z tym poeksperymentować, uwielbiam słuchać, jak to działa dla Ciebie.
rshev
@rshev Myślę, że miałem to samo wcześniej, muszę jeszcze raz sprawdzić. Obecne testy działają dobrze.
Ben Sinclair
1
@LanceSamaria Proponuję użyć viewDidDisappear. Sprawdź, czy pasek nawigacyjny jest dostępny, jeśli nie - nie jest wyświetlany na pasku nawigacyjnym, więc został wyświetlony. if (self.navigationController == nil) {trigger your action}
HotJard,
32

Zrobiłem Swiftwersję z rozszerzeniami z odpowiedzią @JorisKluivers .

Spowoduje to wywołanie zakończenia zakończenia po zakończeniu animacji dla obu pushi pop.

extension UINavigationController {
    func popViewControllerWithHandler(completion: ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.popViewControllerAnimated(true)
        CATransaction.commit()
    }
    func pushViewController(viewController: UIViewController, completion: ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.pushViewController(viewController, animated: true)
        CATransaction.commit()
    }
}
Arbitur
źródło
Dla mnie w iOS 8.4, napisanym w ObjC, blok odpala w połowie animacji. Czy to naprawdę uruchamia się we właściwym momencie, jeśli jest napisane w języku Swift (8.4)?
Julian F. Weinert
Blok uzupełniania @Arbitur jest rzeczywiście wywoływany po wywołaniu popViewControllerlub pushViewController, ale jeśli zaraz potem sprawdzisz, czym jest topViewController, zauważysz, że nadal jest stary, tak jak poplub pushnigdy się nie zdarzyło ...
Bogdan Razvan
@BogdanRazvan zaraz potem co? Czy zamknięcie zakończenia zostanie wywołane po zakończeniu animacji?
Arbitur
17

SWIFT 4.1

extension UINavigationController {
func pushToViewController(_ viewController: UIViewController, animated:Bool = true, completion: @escaping ()->()) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    self.pushViewController(viewController, animated: animated)
    CATransaction.commit()
}

func popViewController(animated:Bool = true, completion: @escaping ()->()) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    self.popViewController(animated: animated)
    CATransaction.commit()
}

func popToViewController(_ viewController: UIViewController, animated:Bool = true, completion: @escaping ()->()) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    self.popToViewController(viewController, animated: animated)
    CATransaction.commit()
}

func popToRootViewController(animated:Bool = true, completion: @escaping ()->()) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    self.popToRootViewController(animated: animated)
    CATransaction.commit()
}
}
Muhammad Waqas
źródło
17

Miałem ten sam problem. A ponieważ musiałem go używać przy wielu okazjach iw ramach łańcuchów bloków uzupełniania, stworzyłem to ogólne rozwiązanie w podklasie UINavigationController:

- (void) navigationController:(UINavigationController *) navigationController didShowViewController:(UIViewController *) viewController animated:(BOOL) animated {
    if (_completion) {
        dispatch_async(dispatch_get_main_queue(),
        ^{
            _completion();
            _completion = nil;
         });
    }
}

- (UIViewController *) popViewControllerAnimated:(BOOL) animated completion:(void (^)()) completion {
    _completion = completion;
    return [super popViewControllerAnimated:animated];
}

Zarozumiały

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

i

@implementation NavigationController {
    void (^_completion)();
}

i

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}
Jos Jong
źródło
1
Naprawdę podoba mi się to rozwiązanie, spróbuję je z kategorią i powiązanym obiektem.
spstanley
@spstanley musisz opublikować ten pod :)
k06a
Wersja szybka -> stackoverflow.com/a/60090678/4010725
WILL K.
15

Nie ma sposobu, aby zrobić to, czego chcesz, po wyjęciu z pudełka. tj. nie ma metody z blokiem uzupełniania do zdejmowania kontrolera widoku ze stosu nawigacji.

To, co zrobiłbym, to umieścić logikę viewDidAppear. Zostanie to wywołane, gdy widok skończy się pojawiać na ekranie. Będzie wywoływany dla wszystkich różnych scenariuszy pojawiania się kontrolera widoku, ale to powinno wystarczyć.

Lub możesz użyć tej UINavigationControllerDelegatemetody, navigationController:didShowViewController:animated:aby zrobić podobną rzecz. Jest to wywoływane, gdy kontroler nawigacji zakończy wypychanie lub otwieranie kontrolera widoku.

mattjgalloway
źródło
Próbowałem tego. Przechowywałem tablicę „indeksów usuniętych wierszy” i za każdym razem, gdy pojawia się widok, sprawdzając, czy coś należy usunąć. Szybko stał się nieporęczny, ale mogę spróbować jeszcze raz. Zastanawiam się, dlaczego Apple zapewnia to dla jednego przejścia, a drugiego nie?
Ben Packard
1
To jest bardzo nowe dismissViewController. Może nadejdzie popViewController. Złóż radar :-).
mattjgalloway
Poważnie, zrób radar. Bardziej prawdopodobne jest, że uda się to, jeśli ludzie o to poproszą.
mattjgalloway
1
To właściwe miejsce, aby o to poprosić. Istnieje opcja klasyfikacji jako „Cecha”.
mattjgalloway
3
Ta odpowiedź nie jest całkowicie poprawna. Chociaż nie możesz ustawić bloku w nowym stylu tak jak on -dismissViewController:animated:completionBlock:, ale możesz uzyskać animację za pośrednictwem delegata kontrolera nawigacji. Po zakończeniu animacji -navigationController:didShowViewController:animated:zostaniesz wezwany do delegata i będziesz mógł zrobić wszystko, co potrzebujesz.
Jason Coco
13

Praca z animacją lub bez niej, a także popToRootViewController:

 // updated for Swift 3.0
extension UINavigationController {

  private func doAfterAnimatingTransition(animated: Bool, completion: @escaping (() -> Void)) {
    if let coordinator = transitionCoordinator, animated {
      coordinator.animate(alongsideTransition: nil, completion: { _ in
        completion()
      })
    } else {
      DispatchQueue.main.async {
        completion()
      }
    }
  }

  func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping (() ->     Void)) {
    pushViewController(viewController, animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }

  func popViewController(animated: Bool, completion: @escaping (() -> Void)) {
    popViewController(animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }

  func popToRootViewController(animated: Bool, completion: @escaping (() -> Void)) {
    popToRootViewController(animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }
}
rshev
źródło
Jest jakiś szczególny powód, dla którego dzwonisz do completion()asynchronicznego?
lewiatan
1
gdy animacja z koordynatorem completionnigdy nie jest wykonywana na tym samym runloopie. to gwarantuje, że completionnigdy nie będzie działać na tym samym runloopie, gdy nie jest animowany. lepiej nie mieć tego rodzaju niespójności.
rshev
11

W oparciu o odpowiedź @ HotJard, kiedy wszystko, czego potrzebujesz, to tylko kilka linii kodu. Szybko i łatwo.

Swift 4 :

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}
Vitalii
źródło
6

Na rok 2018 ...

jeśli masz to ...

    navigationController?.popViewController(animated: false)
    // I want this to happen next, help! ->
    nextStep()

i chcesz dodać uzupełnienie ...

    CATransaction.begin()
    navigationController?.popViewController(animated: true)
    CATransaction.setCompletionBlock({ [weak self] in
       self?.nextStep() })
    CATransaction.commit()

to takie proste.

Przydatna wskazówka ...

Ta sama sprawa dotyczy wygodnego popToViewControllerpołączenia.

Typową rzeczą jest to, że masz na pokładzie stos zillionów ekranów. Kiedy w końcu skończysz, wrócisz z powrotem do ekranu „podstawowego”, a następnie uruchomisz aplikację.

Na ekranie „podstawowym”, aby przejść „całą drogę wstecz”, popToViewController(self

func onboardingStackFinallyComplete() {
    
    CATransaction.begin()
    navigationController?.popToViewController(self, animated: false)
    CATransaction.setCompletionBlock({ [weak self] in
        guard let self = self else { return }
        .. actually launch the main part of the app
    })
    CATransaction.commit()
}
Fattie
źródło
5

Blok uzupełniania jest wywoływany po wywołaniu metody viewDidDisappear na prezentowanym kontrolerze widoku, więc umieszczenie kodu w metodzie viewDidDisappear kontrolera widoku popped powinno działać tak samo jak blok uzupełniania.

rdelmar
źródło
Jasne - z wyjątkiem tego, że musisz obsłużyć wszystkie przypadki, w których widok znika z innego powodu.
Ben Packard
1
@BenPackard, tak, i to samo dotyczy umieszczania go w viewDidAppear w odpowiedzi, którą zaakceptowałeś.
rdelmar
5

Szybka 3 odpowiedź, dzięki tej odpowiedzi: https://stackoverflow.com/a/28232570/3412567

    //MARK:UINavigationController Extension
extension UINavigationController {
    //Same function as "popViewController", but allow us to know when this function ends
    func popViewControllerWithHandler(completion: @escaping ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.popViewController(animated: true)
        CATransaction.commit()
    }
    func pushViewController(viewController: UIViewController, completion: @escaping ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.pushViewController(viewController, animated: true)
        CATransaction.commit()
    }
}
Benobab
źródło
4

Wersja Swift 4 z opcjonalnym parametrem viewController, aby wyświetlić konkretny.

extension UINavigationController {
    func pushViewController(viewController: UIViewController, animated: 
        Bool, completion: @escaping () -> ()) {

        pushViewController(viewController, animated: animated)

        if let coordinator = transitionCoordinator, animated {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
}

func popViewController(viewController: UIViewController? = nil, 
    animated: Bool, completion: @escaping () -> ()) {
        if let viewController = viewController {
            popToViewController(viewController, animated: animated)
        } else {
            popViewController(animated: animated)
        }

        if let coordinator = transitionCoordinator, animated {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
TejAces
źródło
Zaakceptowana odpowiedź wydaje się działać w moim środowisku deweloperskim ze wszystkimi emulatorami / urządzeniami, które posiadam, ale nadal otrzymuję zgłaszane błędy od użytkowników produkcyjnych. Nie jestem pewien, czy to rozwiąże problem produkcyjny, ale pozwolę sobie na głosowanie, aby ktoś mógł spróbować, jeśli otrzyma ten sam problem z zaakceptowanej odpowiedzi.
Sean
4

Wyczyszczono wersję Swift 4 na podstawie tej odpowiedzi .

extension UINavigationController {
    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) {
        self.pushViewController(viewController, animated: animated)
        self.callCompletion(animated: animated, completion: completion)
    }

    func popViewController(animated: Bool, completion: @escaping () -> Void) -> UIViewController? {
        let viewController = self.popViewController(animated: animated)
        self.callCompletion(animated: animated, completion: completion)
        return viewController
    }

    private func callCompletion(animated: Bool, completion: @escaping () -> Void) {
        if animated, let coordinator = self.transitionCoordinator {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
d4Rk
źródło
2

2020 Swift 5.1 sposób

To rozwiązanie gwarantuje, że ukończenie zostanie wykonane po całkowitym zakończeniu popViewController. Możesz to przetestować, wykonując inną operację na NavigationController w zakończeniu: We wszystkich innych rozwiązaniach powyżej UINavigationController jest nadal zajęty operacją popViewController i nie odpowiada.

public class NavigationController: UINavigationController, UINavigationControllerDelegate
{
    private var completion: (() -> Void)?

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        delegate = self
    }

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

    public override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
    {
        if self.completion != nil {
            DispatchQueue.main.async(execute: {
                self.completion?()
                self.completion = nil
            })
        }
    }

    func popViewController(animated: Bool, completion: @escaping () -> Void) -> UIViewController?
    {
        self.completion = completion
        return super.popViewController(animated: animated)
    }
}
WILL K.
źródło
1

Dla kompletności przygotowałem kategorię Cel-C gotową do użycia:

// UINavigationController+CompletionBlock.h

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionBlock)

- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion;

@end
// UINavigationController+CompletionBlock.m

#import "UINavigationController+CompletionBlock.h"

@implementation UINavigationController (CompletionBlock)

- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion {
    [CATransaction begin];
    [CATransaction setCompletionBlock:^{
        completion();
    }];

    UIViewController *vc = [self popViewControllerAnimated:animated];

    [CATransaction commit];

    return vc;
}

@end
Diego Freniche
źródło
1

Osiągnąłem to dokładnie z precyzją za pomocą bloku. Chciałem, aby mój kontroler pobranych wyników pokazywał wiersz dodany przez widok modalny tylko wtedy, gdy całkowicie opuścił ekran, aby użytkownik mógł zobaczyć zachodzącą zmianę. Przygotowując się do przejścia, które jest odpowiedzialne za wyświetlanie kontrolera widoku modalnego, ustawiam blok, który chcę wykonać, gdy modal zniknie. W kontrolerze widoku modalnego nadpisuję viewDidDissapear, a następnie wywołuję blok. Po prostu zaczynam aktualizacje, gdy modal ma się pojawić, i kończę aktualizacje, gdy zniknie, ale to dlatego, że używam NSFetchedResultsController, ale możesz robić, co chcesz, wewnątrz bloku.

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue.identifier isEqualToString:@"addPassword"]){

        UINavigationController* nav = (UINavigationController*)segue.destinationViewController;
        AddPasswordViewController* v = (AddPasswordViewController*)nav.topViewController;

...

        // makes row appear after modal is away.
        [self.tableView beginUpdates];
        [v setViewDidDissapear:^(BOOL animated) {
            [self.tableView endUpdates];
        }];
    }
}

@interface AddPasswordViewController : UITableViewController<UITextFieldDelegate>

...

@property (nonatomic, copy, nullable) void (^viewDidDissapear)(BOOL animated);

@end

@implementation AddPasswordViewController{

...

-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    if(self.viewDidDissapear){
        self.viewDidDissapear(animated);
    }
}

@end
malhal
źródło
1

Użyj następnego rozszerzenia w swoim kodzie: (Swift 4)

import UIKit

extension UINavigationController {

    func popViewController(animated: Bool = true, completion: @escaping () -> Void) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }

    func pushViewController(_ viewController: UIViewController, animated: Bool = true, completion: @escaping () -> Void) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}
Rigoberto Sáenz Imbacuán
źródło