Procedura obsługi zakończenia dla UINavigationController „pushViewController: animated”?

110

Chodzi o stworzenie aplikacji za pomocą a, UINavigationControlleraby zaprezentować następne kontrolery widoku. W iOS5 pojawiła się nowa metoda prezentacji UIViewControllers:

presentViewController:animated:completion:

Teraz pytam mnie, dlaczego nie ma programu obsługi zakończenia UINavigationController? Są po prostu

pushViewController:animated:

Czy jest możliwe stworzenie własnego programu obsługi uzupełniania, takiego jak nowy presentViewController:animated:completion:?

geforce
źródło
2
nie jest to dokładnie to samo, co handlera realizacji, ale viewDidAppear:animated:niech ci na ekranie pojawi się twoje widok kontrolera każdorazowo wykonanie kodu ( viewDidLoadtylko za pierwszym razem kontroler widoku jest ładowany)
Moxy
@Moxy, masz na myśli-(void)viewDidAppear:(BOOL)animated
George'a
2
na rok 2018 ... naprawdę to tylko to: stackoverflow.com/a/43017103/294884
Fattie,

Odpowiedzi:

139

Zobacz odpowiedź par na inne i bardziej aktualne rozwiązanie

UINavigationControlleranimacje są uruchamiane z CoreAnimation, więc sensowne byłoby umieszczenie w nich kodu CATransactioni ustawienie w ten sposób bloku uzupełniania.

Szybki :

Dla szybkiego sugeruję utworzenie rozszerzenia jako takiego

extension UINavigationController {

  public func pushViewController(viewController: UIViewController,
                                 animated: Bool,
                                 completion: @escaping (() -> Void)?) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    pushViewController(viewController, animated: animated)
    CATransaction.commit()
  }

}

Stosowanie:

navigationController?.pushViewController(vc, animated: true) {
  // Animation done
}

Cel C

Nagłówek:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Realizacja:

#import "UINavigationController+CompletionHandler.h"
#import <QuartzCore/QuartzCore.h>

@implementation UINavigationController (CompletionHandler)

- (void)completionhandler_pushViewController:(UIViewController *)viewController 
                                    animated:(BOOL)animated 
                                  completion:(void (^)(void))completion 
{
    [CATransaction begin];
    [CATransaction setCompletionBlock:completion];
    [self pushViewController:viewController animated:animated];
    [CATransaction commit];
}

@end
chrs
źródło
1
Uważam (nie testowałem), że może to dać niedokładne wyniki, jeśli przedstawiony kontroler widoku wyzwoli animacje wewnątrz jego implementacji viewDidLoad lub viewWillAppear. Myślę, że te animacje zostaną uruchomione przed pushViewController: animated: return - w ten sposób program obsługi zakończenia nie zostanie wywołany, dopóki nowo wyzwolone animacje nie zostaną zakończone.
Matt H.
1
@MattH. Przeprowadziłem kilka testów tego wieczoru i wygląda na to, że podczas używania pushViewController:animated:or popViewController:animated, wywołania viewDidLoadi mają viewDidAppearmiejsce w kolejnych cyklach runloop. Mam więc wrażenie, że nawet jeśli te metody wywołują animacje, nie będą one częścią transakcji podanej w przykładowym kodzie. Czy to był twój problem? Bo to rozwiązanie jest bajecznie proste.
LeffelMania
1
Patrząc wstecz na to pytanie, myślę ogólnie o obawach poruszonych przez @MattH. i @LeffelMania podkreślają ważny problem z tym rozwiązaniem - ostatecznie zakłada, że ​​transakcja zostanie zakończona po zakończeniu wypychania, ale framework nie gwarantuje takiego zachowania. Jest to gwarantowane, niż pokazany jest kontroler widoku, o którym mowa didShowViewController. Chociaż to rozwiązanie jest fantastycznie proste, kwestionowałbym jego „przyszłościowość”. Zwłaszcza biorąc pod uwagę zmiany w wyświetlaniu wywołań zwrotnych cyklu życia, które pojawiły się w iOS 7/8
Sam
8
Wydaje się, że nie działa to niezawodnie na urządzeniach z systemem iOS 9. Zobacz odpowiedzi mojego or @ par poniżej, aby uzyskać alternatywę
Mike Sprague
1
@ZevEisenberg zdecydowanie. Moja odpowiedź brzmi: kod dinozaura na tym świecie ~~ 2 lata
chrs
96

iOS 7+ Swift

Swift 4:

// 2018.10.30 par:
//   I've updated this answer with an asynchronous dispatch to the main queue
//   when we're called without animation. This really should have been in the
//   previous solutions I gave but I forgot to add it.
extension UINavigationController {
    public func pushViewController(
        _ viewController: UIViewController,
        animated: Bool,
        completion: @escaping () -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator else {
            DispatchQueue.main.async { completion() }
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }

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

        guard animated, let coordinator = transitionCoordinator else {
            DispatchQueue.main.async { completion() }
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }
}

EDYCJA: Dodałem wersję Swift 3 mojej oryginalnej odpowiedzi. W tej wersji usunąłem przykładową animację pokazaną w wersji Swift 2, ponieważ wydaje się, że zdezorientowała wiele osób.

Swift 3:

import UIKit

// Swift 3 version, no co-animation (alongsideTransition parameter is nil)
extension UINavigationController {
    public func pushViewController(
        _ viewController: UIViewController,
        animated: Bool,
        completion: @escaping (Void) -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator else {
            completion()
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }
}

Swift 2:

import UIKit

// Swift 2 Version, shows example co-animation (status bar update)
extension UINavigationController {
    public func pushViewController(
        viewController: UIViewController,
        animated: Bool,
        completion: Void -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator() else {
            completion()
            return
        }

        coordinator.animateAlongsideTransition(
            // pass nil here or do something animated if you'd like, e.g.:
            { context in
                viewController.setNeedsStatusBarAppearanceUpdate()
            },
            completion: { context in
                completion()
            }
        )
    }
}
par
źródło
1
Czy jest jakiś konkretny powód, dla którego mówisz vc, aby zaktualizował swój pasek stanu? Wydaje się, że działa to dobrze, przekazując niljako blok animacji.
Mike Sprague
2
To przykład czegoś, co możesz zrobić jako równoległą animację (komentarz bezpośrednio nad nim wskazuje, że jest opcjonalny). Podanie też niljest rzeczą całkowicie słuszną.
par
1
@par, czy powinieneś być bardziej defensywny i wywołać zakończenie, gdy transitionCoordinatorjest zero?
Aurelien Porte
@AurelienPorte To świetny chwyt i powiedziałbym, że tak, powinieneś. Zaktualizuję odpowiedź.
par
1
@cbowns Nie jestem tego w 100% pewien, ponieważ nie widziałem tego, ale jeśli nie widzisz, transitionCoordinatorto prawdopodobnie wywołujesz tę funkcję zbyt wcześnie w cyklu życia kontrolera nawigacji. Poczekaj przynajmniej do viewWillAppear()wywołania, zanim spróbujesz wypchnąć kontroler widoku z animacją.
par
28

Na podstawie odpowiedzi par (która była jedyną, która działała z iOS9), ale prostsza iz brakującym innym (co mogłoby doprowadzić do tego, że zakończenie nigdy nie zostało wywołane):

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

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

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

        if animated, let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
Daniel
źródło
Nie działa na mnie. Koordynator przejścia jest dla mnie zerowy.
tcurdt
Pracuje dla mnie. Również ten jest lepszy niż zaakceptowany, ponieważ zakończenie animacji nie zawsze jest tym samym, co ukończenie wypychania.
Anton Plebanovich,
Brakuje DispatchQueue.main.async dla nieanimowanej obudowy. Kontrakt tej metody polega na tym, że procedura obsługi zakończenia jest wywoływana asynchronicznie, nie należy tego naruszać, ponieważ może to prowadzić do subtelnych błędów.
Werner Altewischer
24

Obecnie UINavigationControllernie obsługuje tego. Ale jest to UINavigationControllerDelegate, czego możesz użyć.

Łatwym sposobem osiągnięcia tego jest tworzenie podklas UINavigationControlleri dodanie właściwości bloku uzupełniania:

@interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>

@property (nonatomic,copy) dispatch_block_t completionBlock;

@end


@implementation PbNavigationController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.delegate = self;
    }
    return self;
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@", viewController);

    if (self.completionBlock) {
        self.completionBlock();
        self.completionBlock = nil;
    }
}

@end

Przed naciśnięciem nowego kontrolera widoku musiałbyś ustawić blok uzupełniania:

UIViewController *vc = ...;
((PbNavigationController *)self.navigationController).completionBlock = ^ {
    NSLog(@"COMPLETED");
};
[self.navigationController pushViewController:vc animated:YES];

Ta nowa podklasa może być przypisana w programie Interface Builder lub używana programowo w następujący sposób:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
źródło
8
Dodanie listy bloków uzupełniania odwzorowanych na kontrolery widoku prawdopodobnie uczyniłoby to najbardziej użytecznym, a nowa metoda, być może nazwana pushViewController:animated:completion:, uczyniłaby to eleganckim rozwiązaniem.
Hiperbola
1
Uwaga na 2018 to naprawdę tylko to ... stackoverflow.com/a/43017103/294884
Fattie,
8

Oto wersja Swift 4 z Pop.

extension UINavigationController {
    public func pushViewController(viewController: UIViewController,
                                   animated: Bool,
                                   completion: (() -> Void)?) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }

    public func popViewController(animated: Bool,
                                  completion: (() -> Void)?) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }
}

Na wypadek, gdyby ktoś tego potrzebował.

Francois Nadeau
źródło
Jeśli wykonasz prosty test, zobaczysz, że blok ukończenia jest uruchamiany przed zakończeniem animacji. Więc to prawdopodobnie nie zapewnia tego, czego wielu szuka.
podkowa 7
7

Aby rozwinąć odpowiedź @Klaas (iw rezultacie tego pytania) dodałem bloki uzupełniania bezpośrednio do metody push:

@interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>

@property (nonatomic,copy) dispatch_block_t completionBlock;
@property (nonatomic,strong) UIViewController * pushedVC;

@end


@implementation PbNavigationController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.delegate = self;
    }
    return self;
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@", viewController);

    if (self.completionBlock && self.pushedVC == viewController) {
        self.completionBlock();
    }
    self.completionBlock = nil;
    self.pushedVC = nil;
}

-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (self.pushedVC != viewController) {
        self.pushedVC = nil;
        self.completionBlock = nil;
    }
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(dispatch_block_t)completion {
    self.pushedVC = viewController;
    self.completionBlock = completion;
    [self pushViewController:viewController animated:animated];
}

@end

Do wykorzystania w następujący sposób:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
źródło
Znakomity. Wielkie dzięki
Petar
if... (self.pushedVC == viewController) {jest nieprawidłowe. Musisz przetestować równość między obiektami, używając isEqual:, np.[self.pushedVC isEqual:viewController]
Evan R
@EvanR, który jest prawdopodobnie bardziej poprawny technicznie tak. czy zauważyłeś błąd w porównaniu instancji w inny sposób?
Sam,
@Sam nie konkretnie w tym przykładzie (nie zaimplementował go), ale zdecydowanie w testowaniu równości z innymi obiektami - zobacz dokumentację Apple na ten temat: developer.apple.com/library/ios/documentation/General/… . Czy twoja metoda porównania zawsze działa w tym przypadku?
Evan R
Nie widziałem, żeby to nie działało lub zmieniłbym odpowiedź. O ile wiem, iOS nie robi nic sprytnego, aby odtworzyć kontrolery widoku, takie jak Android robi z działaniami. ale tak, isEqualprawdopodobnie byłoby bardziej poprawne technicznie, gdyby kiedykolwiek to zrobili.
Sam,
5

Od iOS 7.0 możesz użyć, UIViewControllerTransitionCoordinatoraby dodać blok uzupełniania wypychania:

UINavigationController *nav = self.navigationController;
[nav pushViewController:vc animated:YES];

id<UIViewControllerTransitionCoordinator> coordinator = vc.transitionCoordinator;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {

} completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
    NSLog(@"push completed");
}];
wj2061
źródło
1
To nie jest całkiem tak samo jak UINavigationController Push, pop, itp
Jon Willis
3

Swift 2.0

extension UINavigationController : UINavigationControllerDelegate {
    private struct AssociatedKeys {
        static var currentCompletioObjectHandle = "currentCompletioObjectHandle"
    }
    typealias Completion = @convention(block) (UIViewController)->()
    var completionBlock:Completion?{
        get{
            let chBlock = unsafeBitCast(objc_getAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle), Completion.self)
            return chBlock as Completion
        }set{
            if let newValue = newValue {
                let newValueObj : AnyObject = unsafeBitCast(newValue, AnyObject.self)
                objc_setAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle, newValueObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    func popToViewController(animated: Bool,comp:Completion){
        if (self.delegate == nil){
            self.delegate = self
        }
        completionBlock = comp
        self.popViewControllerAnimated(true)
    }
    func pushViewController(viewController: UIViewController, comp:Completion) {
        if (self.delegate == nil){
            self.delegate = self
        }
        completionBlock = comp
        self.pushViewController(viewController, animated: true)
    }

    public func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool){
        if let comp = completionBlock{
            comp(viewController)
            completionBlock = nil
            self.delegate = nil
        }
    }
}
rahul_send89
źródło
2

Potrzeba trochę więcej pracy, aby dodać to zachowanie i zachować możliwość ustawiania delegata zewnętrznego.

Oto udokumentowana implementacja, która zachowuje funkcjonalność delegata:

LBXCompletingNavigationController

nzeltzer
źródło