Abschlussblock für popViewController


113

Wenn Sie einen Modal View Controller mit schließen dismissViewController, besteht die Möglichkeit, einen Abschlussblock bereitzustellen. Gibt es ein ähnliches Äquivalent für popViewController?

Das Vervollständigungsargument ist sehr praktisch. Zum Beispiel kann ich damit das Entfernen einer Zeile aus einer Tabellenansicht verzögern, bis das Modal nicht mehr auf dem Bildschirm angezeigt wird, sodass der Benutzer die Zeilenanimation sehen kann. Bei der Rückkehr von einem Push-View-Controller möchte ich die gleiche Gelegenheit.

Ich habe versucht, popViewControllereinen UIViewAnimationsblock zu platzieren, in dem ich Zugriff auf einen Abschlussblock habe. Dies führt jedoch zu einigen unerwünschten Nebenwirkungen auf die Ansicht, auf die zugegriffen wird.

Welche Problemumgehungen gibt es, wenn keine solche Methode verfügbar ist?


stackoverflow.com/a/33767837/2774520 Ich denke, dieser Weg ist der einheimischste
Oleksii Nezhyborets


3
Für 2018 ist dies sehr einfach und Standard: stackoverflow.com/a/43017103/294884
Fattie

Antworten:


199

Ich weiß, dass eine Antwort vor über zwei Jahren akzeptiert wurde, diese Antwort ist jedoch unvollständig.

Es gibt keine Möglichkeit, das zu tun, was Sie möchten

Dies ist technisch korrekt, da die UINavigationControllerAPI hierfür keine Optionen bietet. Mit dem CoreAnimation-Framework können Sie der zugrunde liegenden Animation jedoch einen Abschlussblock hinzufügen:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

Der Abschlussblock wird aufgerufen, sobald die von popViewControllerAnimated:Ende verwendete Animation beendet ist. Diese Funktionalität ist seit iOS 4 verfügbar.


5
Ich habe dies in eine Erweiterung von UINavigationController in Swift extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
eingefügt

1
Scheint für mich nicht zu funktionieren, wenn ich CompletionHandler auf Entlassung von ViewController ausführe, ist die Ansicht, die es präsentiert hat, Teil der Ansichtshierarchie. Wenn ich dasselbe mit der CATransaction mache, erhalte ich eine Warnung, dass die Ansicht nicht Teil der Ansichtshierarchie ist.
moger777

1
OK, sieht aus wie Ihre Arbeit, wenn Sie den Start- und Abschlussblock umkehren. Entschuldigung für die
Abwertung,

7
Ja, das schien fantastisch zu sein, aber es scheint nicht zu funktionieren (zumindest unter iOS 8). Der Abschlussblock wird sofort aufgerufen. Wahrscheinlich aufgrund der Mischung von Kernanimationen mit Animationen im UIView-Stil.
Stuckj

5
Dies funktioniert nicht
Durazno

51

Für iOS9 SWIFT-Version - funktioniert wie ein Zauber (wurde nicht für frühere Versionen getestet). Basierend auf dieser Antwort

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()
        }
    }
}

Funktioniert nicht, wenn nicht animiert, sollte beim nächsten Runloop abgeschlossen werden, um es richtig zu machen.
Rshev

@rshev warum beim nächsten Runloop?
Ben Sinclair

@Andy Soweit ich mich erinnere, hatte ich zu diesem Zeitpunkt noch nichts propagiert. Probieren Sie es aus und hören Sie gerne, wie es bei Ihnen funktioniert.
Rshev

@rshev Ich glaube, ich hatte es vorher genauso, ich muss es noch einmal überprüfen. Aktuelle Tests laufen gut.
Ben Sinclair

1
@LanceSamaria Ich schlage vor, viewDidDisappear zu verwenden. Überprüfen Sie, ob die Navigationsleiste verfügbar ist. Wenn nicht, wird sie nicht in der Navigationsleiste angezeigt, sodass sie geöffnet wurde. if (self.navigationController == nil) {Aktion auslösen}
HotJard

32

Ich habe eine SwiftVersion mit Erweiterungen mit der Antwort @JorisKluivers erstellt .

Dies ruft einen Abschluss ab, nachdem die Animation für pushund abgeschlossen wurde 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()
    }
}

In iOS 8.4, das in ObjC geschrieben wurde, wird der Block auf halber Strecke der Animation ausgelöst. Wird dies wirklich im richtigen Moment ausgelöst, wenn es in Swift (8.4) geschrieben ist?
Julian F. Weinert

@Arbitur Abschlussblock wird in der Tat nach dem Aufruf popViewControlleroder aufgerufen pushViewController, aber wenn Sie überprüfen, was der topViewController direkt danach ist, werden Sie feststellen, dass es immer noch der alte ist, genau wie popoder pushnie passiert ...
Bogdan Razvan

@ BogdanRazvan gleich danach was? Wird Ihr Abschluss nach Abschluss der Animation aufgerufen?
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()
}
}

17

Ich hatte das gleiche Problem. Und weil ich es mehrmals und innerhalb von Ketten von Abschlussblöcken verwenden musste, habe ich diese generische Lösung in einer UINavigationController-Unterklasse erstellt:

- (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];
}

Vorausgesetzt

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

und

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

und

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}

1
Diese Lösung gefällt mir sehr gut. Ich werde sie mit einer Kategorie und einem zugehörigen Objekt ausprobieren.
Spstanley

@spstanley Sie müssen diesen Pod veröffentlichen :)
k06a


15

Es gibt keine Möglichkeit, das zu tun, was Sie möchten. Das heißt, es gibt keine Methode mit einem Abschlussblock zum Löschen eines Ansichtscontrollers von einem Navigationsstapel.

Was ich tun würde, ist die Logik einzubauen viewDidAppear. Dies wird aufgerufen, wenn die Ansicht nicht mehr auf dem Bildschirm angezeigt wird. Es wird für alle verschiedenen Szenarien des View Controllers aufgerufen, aber das sollte in Ordnung sein.

Oder Sie könnten die UINavigationControllerDelegateMethode verwenden, navigationController:didShowViewController:animated:um etwas Ähnliches zu tun. Dies wird aufgerufen, wenn der Navigations-Controller einen Ansichts-Controller nicht mehr gedrückt oder geöffnet hat.


Ich habe es versucht. Ich habe ein Array von 'gelöschten Zeilenindizes' gespeichert und bei jeder Anzeige überprüft, ob etwas entfernt werden muss. Es wurde schnell unhandlich, aber ich könnte es noch einmal versuchen. Ich frage mich, warum Apple es für einen Übergang bereitstellt, aber nicht für den anderen?
Ben Packard

1
Es ist nur sehr neu auf der dismissViewController. Vielleicht kommt es zu popViewController. Datei ein Radar :-).
Mattjgalloway

Im Ernst, archivieren Sie ein Radar. Es ist wahrscheinlicher, es zu schaffen, wenn Leute danach fragen.
Mattjgalloway

1
Das ist der richtige Ort, um danach zu fragen. Es gibt eine Option für die Klassifizierung als "Feature".
Mattjgalloway

3
Diese Antwort ist nicht ganz richtig. Sie können den Block im neuen Stil zwar nicht wie aktiviert einstellen -dismissViewController:animated:completionBlock:, aber Sie können die Animation über den Delegaten des Navigationscontrollers abrufen. Nachdem die Animation abgeschlossen ist, -navigationController:didShowViewController:animated:wird der Delegierte aufgerufen und Sie können genau dort tun, was Sie benötigen.
Jason Coco

13

Arbeiten mit oder ohne Animation richtig und beinhaltet auch 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)
  }
}

Gibt es einen bestimmten Grund, warum Sie den completion()Async aufrufen ?
Leviathan

1
Wenn die Animation mit dem Koordinator completionniemals auf demselben Runloop ausgeführt wird. Dies garantiert, dass completionniemals auf demselben Runloop ausgeführt wird, wenn nicht animiert wird. Es ist besser, diese Art von Inkonsistenz nicht zu haben.
Rshev

11

Basierend auf der Antwort von @ HotJard, wenn Sie nur ein paar Codezeilen benötigen. Schnell und einfach.

Swift 4 :

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}

6

Für 2018 ...

wenn du das hast ...

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

und Sie möchten eine Vervollständigung hinzufügen ...

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

so einfach ist das

Praktischer Tipp ...

Es ist das gleiche Angebot für den praktischen popToViewControllerAnruf.

Eine typische Sache ist, dass Sie einen Onboarding-Stapel von zig Bildschirmen haben. Wenn Sie fertig sind, kehren Sie vollständig zu Ihrem "Basis" -Bildschirm zurück und starten schließlich die App.

Also im "Basis" -Bildschirm, um "den ganzen Weg zurück" zu gehen, 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()
}

5

Der Abschlussblock wird aufgerufen, nachdem die viewDidDisappear-Methode auf dem dargestellten Ansichtscontroller aufgerufen wurde. Daher sollte das Einfügen von Code in die viewDidDisappear-Methode des Popup-Ansichtscontrollers genauso funktionieren wie ein Abschlussblock.


Sicher - außer dann müssen Sie alle Fälle behandeln, in denen die Ansicht aus einem anderen Grund verschwindet.
Ben Packard

1
@BenPackard, ja, und das Gleiche gilt für das Einfügen in viewDidAppear in der von Ihnen akzeptierten Antwort.
Rdelmar

5

Swift 3-Antwort dank dieser Antwort: 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()
    }
}

4

Swift 4-Version mit optionalem viewController-Parameter, um zu einem bestimmten zu wechseln.

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()
        }
    }
}

Die akzeptierte Antwort scheint in meiner Entwicklungsumgebung mit allen Emulatoren / Geräten zu funktionieren, die ich habe, aber ich bekomme immer noch Fehler von Produktionsbenutzern. Ich bin mir nicht sicher, ob dies das Produktionsproblem lösen wird, aber ich möchte es nur bewerten, damit jemand es versuchen kann, wenn er das gleiche Problem aus der akzeptierten Antwort erhält.
Sean

4

Die Swift 4- Version wurde basierend auf dieser Antwort bereinigt .

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()
        }
    }
}


2

2020 Swift 5.1 Weg

Diese Lösung garantiert, dass der Abschluss ausgeführt wird, nachdem popViewController vollständig abgeschlossen ist. Sie können es testen, indem Sie einen weiteren Vorgang auf dem NavigationController ausführen: In allen anderen oben genannten Lösungen ist der UINavigationController noch mit dem popViewController-Vorgang beschäftigt und antwortet nicht.

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

1

Der Vollständigkeit halber habe ich eine Objective-C-Kategorie einsatzbereit gemacht:

// 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

1

Genau das habe ich mit einem Block präzise erreicht. Ich wollte, dass mein Controller für abgerufene Ergebnisse die von der modalen Ansicht hinzugefügte Zeile nur anzeigt, wenn sie den Bildschirm vollständig verlassen hat, damit der Benutzer die Änderung sehen kann. In Vorbereitung auf den Übergang, der für die Anzeige des Modal View Controllers verantwortlich ist, setze ich den Block, den ich ausführen möchte, wenn das Modal verschwindet. Und im Modal View Controller überschreibe ich viewDidDissapear und rufe dann den Block auf. Ich beginne einfach mit Updates, wenn das Modal angezeigt wird, und beende Updates, wenn es verschwindet. Dies liegt jedoch daran, dass ich einen NSFetchedResultsController verwende. Sie können jedoch innerhalb des Blocks tun, was Sie möchten.

-(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

1

Verwenden Sie die nächste Erweiterung Ihres Codes: (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()
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.