Penangan penyelesaian untuk UINavigationController “pushViewController: animated”?

110

Saya tentang membuat aplikasi menggunakan UINavigationControlleruntuk menyajikan pengontrol tampilan berikutnya. Dengan iOS5 ada metode baru untuk mempresentasikan UIViewControllers:

presentViewController:animated:completion:

Sekarang saya bertanya mengapa tidak ada penangan penyelesaian untuk UINavigationController? Hanya ada

pushViewController:animated:

Apakah mungkin untuk membuat penangan penyelesaian saya sendiri seperti yang baru presentViewController:animated:completion:?

geforce
sumber
2
tidak persis sama dengan penangan penyelesaian tetapi viewDidAppear:animated:memungkinkan Anda mengeksekusi kode setiap kali pengontrol tampilan Anda muncul di layar ( viewDidLoadhanya saat pengontrol tampilan Anda dimuat pertama kali)
Moxy
@Moxy, maksud Anda-(void)viewDidAppear:(BOOL)animated
George
2
untuk 2018 ... benar-benar hanya ini: stackoverflow.com/a/43017103/294884
Fattie

Jawaban:

139

Lihat jawaban par untuk solusi lain yang lebih mutakhir

UINavigationControlleranimasi dijalankan dengan CoreAnimation, jadi masuk akal untuk merangkum kode di dalamnya CATransactiondan dengan demikian menetapkan blok penyelesaian.

Swift :

Untuk cepat saya sarankan membuat ekstensi seperti itu

extension UINavigationController {

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

}

Pemakaian:

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

Objective-C

Header:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Penerapan:

#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
sumber
1
Saya percaya (belum diuji) bahwa ini dapat memberikan hasil yang tidak akurat jika pengontrol tampilan yang disajikan memicu animasi di dalam implementasi viewDidLoad atau viewWillAppear. Saya pikir animasi tersebut akan dimulai sebelum pushViewController: animated: return - dengan demikian, penangan penyelesaian tidak akan dipanggil sampai animasi yang baru dipicu selesai.
Matt H.
1
@Tokopedia Apakah beberapa pengujian malam ini dan sepertinya saat menggunakan pushViewController:animated:atau popViewController:animated, panggilan viewDidLoaddan viewDidAppearterjadi dalam siklus runloop berikutnya. Jadi kesan saya adalah bahwa meskipun metode tersebut memang memanggil animasi, mereka tidak akan menjadi bagian dari transaksi yang disediakan dalam contoh kode. Apakah itu yang menjadi perhatian Anda? Karena solusi ini sangat sederhana.
LeffelMania
1
Melihat kembali pertanyaan ini, saya pikir secara umum kekhawatiran yang disebutkan oleh @MattH. dan @LeffelMania menyoroti masalah yang valid dengan solusi ini - ini pada akhirnya mengasumsikan transaksi akan selesai setelah push selesai, tetapi kerangka kerja tidak menjamin perilaku ini. Ini dijamin daripada pengontrol tampilan yang dipermasalahkan ditampilkan di didShowViewControllersekalipun. Sementara solusi ini sangat sederhana, saya akan mempertanyakan "bukti masa depan". Terutama mengingat perubahan untuk melihat callback siklus hidup yang disertakan dengan ios7 / 8
Sam
8
Ini sepertinya tidak berfungsi dengan andal pada perangkat iOS 9. Lihat jawaban saya atau @ par di bawah ini untuk alternatif
Mike Sprague
1
@ZevEisenberg pasti. Jawaban saya adalah kode dinosaurus di dunia ini ~~ 2 tahun
chrs
96

iOS 7+ Swift

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

EDIT: Saya telah menambahkan versi Swift 3 dari jawaban asli saya. Dalam versi ini saya telah menghapus contoh co-animasi yang ditampilkan di versi Swift 2 karena tampaknya telah membingungkan banyak orang.

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

Cepat 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
sumber
1
Apakah ada alasan khusus mengapa Anda memberi tahu vc untuk memperbarui bilah statusnya? Ini tampaknya bekerja dengan baik nilsebagai blok animasi.
Mike Sprague
2
Ini adalah contoh dari sesuatu yang mungkin Anda lakukan sebagai animasi paralel (komentar tepat di atasnya menunjukkan itu opsional). Mengoper niladalah hal yang sangat valid untuk dilakukan juga.
par
1
@ Par, Haruskah Anda lebih defensif dan menyebut penyelesaian ketika transitionCoordinatoris nihil?
Aurelien Porte
@AurelienPorte Itu tangkapan yang bagus dan saya akan mengatakan ya, Anda harus. Saya akan memperbarui jawabannya.
par
1
@cbowns Saya tidak 100% yakin tentang ini karena saya belum melihat ini terjadi, tetapi jika Anda tidak melihatnya transitionCoordinatormaka kemungkinan Anda memanggil fungsi ini terlalu dini dalam siklus hidup pengontrol navigasi. Tunggu setidaknya hingga viewWillAppear()dipanggil sebelum mencoba mendorong pengontrol tampilan dengan animasi.
par
28

Berdasarkan jawaban par (yang merupakan satu-satunya yang bekerja dengan iOS9), tetapi lebih sederhana dan tanpa hal lain (yang bisa menyebabkan penyelesaian tidak pernah dipanggil):

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
sumber
Tidak bekerja untuk saya. TransisiCoordinator adalah nol bagi saya.
tcurdt
Bekerja untuk saya. Juga yang ini lebih baik daripada yang diterima karena penyelesaian animasi tidak selalu sama dengan penyelesaian push.
Anton Plebanovich
Anda kehilangan DispatchQueue.main.async untuk kasus non animasi. Kontrak dari metode ini adalah bahwa penangan penyelesaian disebut secara asinkron, Anda tidak boleh melanggar ini karena dapat menyebabkan bug halus.
Werner Altewischer
24

Saat UINavigationControllerini tidak mendukung ini. Tapi ada UINavigationControllerDelegateyang bisa Anda gunakan.

Cara mudah untuk melakukannya adalah dengan membuat subclass UINavigationControllerdan menambahkan properti blok penyelesaian:

@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

Sebelum mendorong pengontrol tampilan baru, Anda harus mengatur blok penyelesaian:

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

Subclass baru ini dapat ditetapkan di Interface Builder atau digunakan secara terprogram seperti ini:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
sumber
8
Menambahkan daftar blok penyelesaian yang dipetakan untuk melihat pengontrol mungkin akan membuat ini paling berguna, dan metode baru, mungkin dipanggil pushViewController:animated:completion:akan membuat ini menjadi solusi yang elegan.
Hiperbola
1
NB untuk 2018 benar-benar hanya ini ... stackoverflow.com/a/43017103/294884
Fattie
8

Ini adalah versi Swift 4 dengan 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()
    }
}

Kalau-kalau ada orang lain yang membutuhkan ini.

Francois Nadeau
sumber
Jika Anda menjalankan pengujian sederhana tentang ini, Anda akan menemukan bahwa blok penyelesaian diaktifkan sebelum animasi selesai. Jadi ini mungkin tidak memberikan apa yang dicari banyak orang.
tapal
7

Untuk memperluas jawaban @Klaas (dan sebagai hasil dari pertanyaan ini ) saya telah menambahkan blok penyelesaian langsung ke metode 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

Untuk digunakan sebagai berikut:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
sumber
Cemerlang. Terima kasih banyak
Petar
if... (self.pushedVC == viewController) {salah. Anda perlu menguji kesetaraan antar objek dengan menggunakan isEqual:, yaitu,[self.pushedVC isEqual:viewController]
Evan R
@ EvanR yang mungkin lebih teknis benar ya. Pernahkah Anda melihat kesalahan dalam membandingkan instance dengan cara lain?
Sam
@Sam tidak secara khusus dengan contoh ini (tidak menerapkannya) tetapi pasti dalam menguji kesetaraan dengan objek lain — lihat dokumen Apple tentang ini: developer.apple.com/library/ios/documentation/General/… . Apakah metode perbandingan Anda selalu berhasil dalam kasus ini?
Evan R
Saya tidak melihatnya tidak berhasil atau saya akan mengubah jawaban saya. Sejauh yang saya tahu iOS tidak melakukan sesuatu yang pintar untuk membuat ulang pengontrol tampilan seperti yang dilakukan android dengan aktivitas. tapi ya, isEqualmungkin akan lebih benar secara teknis jika mereka pernah melakukannya.
Sam
5

Sejak iOS 7.0, Anda dapat menggunakan UIViewControllerTransitionCoordinatoruntuk menambahkan blok penyelesaian push:

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.dll
sumber
1
Ini tidak persis sama dengan UINavigationController push, pop, dll.
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
sumber
2

Diperlukan lebih banyak proses untuk menambahkan perilaku ini dan mempertahankan kemampuan untuk menetapkan delegasi eksternal.

Berikut adalah implementasi terdokumentasi yang mempertahankan fungsionalitas delegasi:

LBXCompletingNavigationController

nzeltzer.dll
sumber