UINavigationController "pushViewController: animasyonlu" için tamamlama işleyicisi?


110

UINavigationControllerSonraki görünüm denetleyicilerini sunmak için a kullanarak bir uygulama oluşturmak üzereyim . İOS5 ile sunmanın yeni bir yöntemi var UIViewControllers:

presentViewController:animated:completion:

Şimdi bana soruyorum neden bir tamamlama işleyicisi yok UINavigationController? Sadece var

pushViewController:animated:

Yeni gibi kendi tamamlama işleyicimi yaratmam mümkün mü presentViewController:animated:completion:?


2
tamamlama işleyicisiyle tam olarak aynı şey değil, ancak viewDidAppear:animated:görüntüleme denetleyiciniz ekranda her göründüğünde ( viewDidLoadyalnızca görüntüleme denetleyiciniz ilk kez yüklendiğinde) kodu çalıştırmanıza izin verin
Moxy

@Moxy, demek istiyorsun-(void)viewDidAppear:(BOOL)animated
George

2
için 2018 : ... gerçekten sadece bu var stackoverflow.com/a/43017103/294884
fattie

Yanıtlar:


139

Başka ve daha güncel bir çözüm için verilen cevaba bakın

UINavigationControlleranimasyonlar birlikte çalıştırılır CoreAnimation, bu CATransactionnedenle içindeki kodu kapsüllemek ve böylece bir tamamlama bloğu ayarlamak mantıklı olacaktır .

Swift :

Hızlı bir şekilde böyle bir uzantı oluşturmanızı öneririm

extension UINavigationController {

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

}

Kullanımı:

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

Objective-C

Başlık:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Uygulama:

#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

1
Sunulan görünüm denetleyicisi, viewDidLoad veya viewWillAppear uygulamaları içindeki animasyonları tetiklerse, bunun yanlış sonuçlar sağlayacağına inanıyorum (test etmedim). Sanırım bu animasyonlar pushViewController: animated: döner - bu nedenle, tamamlama işleyicisi yeni tetiklenen animasyonlar bitene kadar çağrılmayacaktır.
Matt

1
@MattH. Bu akşam bir çift test yaptı ve sanki pushViewController:animated:veya kullanılırken popViewController:animatedve viewDidLoadve viewDidAppearçağrıları sonraki runloop döngülerinde gerçekleşiyor gibi görünüyor . Dolayısıyla benim izlenimim, bu yöntemler animasyonları çağırsa bile, kod örneğinde sağlanan işlemin parçası olmayacaklarıdır. Endişen bu muydu? Çünkü bu çözüm inanılmaz derecede basit.
LeffelMania

1
Bu soruya dönüp baktığımda, genel olarak @MattH tarafından bahsedilen endişeleri düşünüyorum. ve @LeffelMania bu çözümle ilgili geçerli bir sorunu vurgulamaktadır - sonuçta işlemin itme tamamlandıktan sonra tamamlanacağını varsayar, ancak çerçeve bu davranışı garanti etmez. Yine de söz konusu görünüm denetleyicisinin gösterildiğinden daha garantilidir didShowViewController. Bu çözüm fevkalade basit olsa da, "geleceğe dönük olup olmadığını" sorgulardım. Özellikle ios7 / 8 ile gelen yaşam döngüsü geri aramalarını görüntüleme değişiklikleri göz önüne alındığında
Sam

8
Bu, iOS 9 cihazlarında güvenilir bir şekilde çalışmıyor gibi görünüyor. Bir alternatif için aşağıda my or @ par'ın yanıtlarına bakın
Mike Sprague

1
@ZevEisenberg kesinlikle. Benim cevabım bu dünyada dinozor kodu ~~ 2 yaşında
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() }
    }
}

DÜZENLEME: Orijinal cevabımın Swift 3 versiyonunu ekledim. Bu sürümde, Swift 2 sürümünde gösterilen örnek ortak animasyonu kaldırdım, çünkü birçok insanın kafasını karıştırmış görünüyor.

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

1
Vc'ye durum çubuğunu güncellemesini söylemenizin belirli bir nedeni var mı? Bu nil, animasyon bloğu olarak geçerken iyi çalışıyor gibi görünüyor .
Mike Sprague

2
Bu, paralel animasyon olarak yapabileceğiniz bir şeye örnektir (hemen üzerindeki yorum, isteğe bağlı olduğunu belirtir). Pas vermek nilde son derece geçerli bir şey.
par

1
@par, daha savunmacı olmalı ve transitionCoordinatorsıfır olduğunda tamamlamayı mı çağırmalısınız ?
Aurelien Porte

@AurelienPorte Bu harika bir av ve evet derim, yapmalısın. Cevabı güncelleyeceğim.
par

1
@cbowns Bunun olduğunu görmediğim için% 100 emin değilim, ancak bir görmüyorsanız, transitionCoordinatormuhtemelen bu işlevi gezinme denetleyicisinin yaşam döngüsünde çok erken çağırıyorsunuzdur. viewWillAppear()Animasyonla bir görünüm denetleyicisini itmeye çalışmadan önce en azından çağrılıncaya kadar bekleyin .
par

28

Dayanarak par cevap , ancak daha basit ve birlikte (sadece iOS9 ile çalışmış biriydi) bir başka eksik (hiçbir zaman çağrılan tamamlanmasına yol açmıştır olabilir):

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

Benim için çalışmıyor. Geçiş Koordinatörü benim için sıfır.
tcurdt

Benim için çalışıyor. Ayrıca bu, kabul edilenden daha iyidir çünkü animasyon tamamlama her zaman itme tamamlama ile aynı değildir.
Anton Plebanovich

Animasyonsuz servis talebi için DispatchQueue.main.async eksik. Bu yöntemin sözleşmesi, tamamlama işleyicisinin eşzamansız olarak çağrılmasıdır, bunu ihlal etmemelisiniz çünkü bu, ince hatalara yol açabilir.
Werner Altewischer

24

Şu anda UINavigationControllerbunu desteklemiyor. Ama UINavigationControllerDelegatekullanabileceğin şey var.

Bunu gerçekleştirmenin kolay bir yolu, alt sınıflandırma UINavigationControllerve bir tamamlama bloğu özelliği eklemektir:

@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

Yeni görünüm denetleyicisine basmadan önce tamamlama bloğunu ayarlamanız gerekir:

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

Bu yeni alt sınıf, Interface Builder'da atanabilir veya şu şekilde programlı olarak kullanılabilir:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];

8
Denetleyicileri görüntülemek için eşlenen tamamlama bloklarının bir listesinin eklenmesi bunu muhtemelen en kullanışlı hale getirecektir ve belki de adı verilen yeni bir yöntem, pushViewController:animated:completion:bunu zarif bir çözüm haline getirecektir.
Hyperbole

1
Not 2018 için gerçekten sadece bu ... stackoverflow.com/a/43017103/294884
Fattie

8

İşte Pop ile Swift 4 versiyonu.

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

Başkasının buna ihtiyacı olması durumunda.


Bununla ilgili basit bir test çalıştırırsanız, tamamlama bloğunun animasyon bitmeden önce patladığını göreceksiniz. Yani bu muhtemelen pek çoğunun aradığı şeyi sağlamaz.
at

7

@Klaas'ın cevabını genişletmek için (ve bu sorunun bir sonucu olarak ) tamamlama bloklarını doğrudan push yöntemine ekledim:

@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

Aşağıdaki gibi kullanılacaktır:

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

Parlak. Çok teşekkürler
Petar

if... (self.pushedVC == viewController) {yanlış. Nesneler arasındaki eşitliği kullanarak isEqual:, yani[self.pushedVC isEqual:viewController]
Evan R

@EvanR muhtemelen teknik olarak daha doğru evet. Örnekleri diğer şekilde karşılaştırırken bir hata gördünüz mü?
Sam

@ Bu örnekle özel olarak değil (uygulamadım) ama kesinlikle diğer nesnelerle eşitliği test ederken samimiyet edin - bu konudaki Apple belgelerine bakın: developer.apple.com/library/ios/documentation/General/… . Karşılaştırma yönteminiz bu durumda her zaman işe yarar mı?
Evan R

İşe yaramadığını görmedim yoksa cevabımı değiştirirdim. Bildiğim kadarıyla iOS, android'in etkinliklerle yaptığı gibi görünüm denetleyicilerini yeniden oluşturmak için akıllıca bir şey yapmıyor. ama evet, isEqualyapmış olsalar muhtemelen teknik olarak daha doğru olurdu.
Sam

5

İOS 7.0'dan beri, UIViewControllerTransitionCoordinatorbir itme tamamlama bloğu eklemek için kullanabilirsiniz :

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");
}];

1
Bu vs. UINavigationController itme, pop, aynı şey değildir
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
        }
    }
}

2

Bu davranışı eklemek ve harici bir temsilci belirleme yeteneğini korumak için biraz daha fazla kanal çalışması gerekir.

Temsilci işlevselliğini koruyan belgelenmiş bir uygulama:

LBXCompletingNavigationController

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.