Обработчик завершения для UINavigationController «pushViewController: animated»?

110

Я собираюсь создать приложение, используя UINavigationControllerдля представления следующих контроллеров представления. В iOS5 появился новый метод представления UIViewControllers:

presentViewController:animated:completion:

Теперь я спрашиваю себя, почему нет обработчика завершения UINavigationController? Есть только

pushViewController:animated:

Можно ли создать свой собственный обработчик завершения, как новый presentViewController:animated:completion:?

GeForce
источник
2
не совсем то же самое, что обработчик завершения, но viewDidAppear:animated:позвольте вам выполнять код каждый раз, когда ваш контроллер представления появляется на экране ( viewDidLoadтолько при первой загрузке вашего контроллера представления)
Moxy
@Moxy, вы имеете в виду-(void)viewDidAppear:(BOOL)animated
Джордж
2
на 2018 год ... на самом деле это просто: stackoverflow.com/a/43017103/294884
Fattie 04

Ответы:

139

См . Ответ par для получения еще одного и более современного решения

UINavigationControllerанимации запускаются с CoreAnimation, поэтому было бы разумно инкапсулировать код внутри CATransactionи, таким образом, установить блок завершения.

Swift :

Для быстрого предлагаю создать расширение как таковое

extension UINavigationController {

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

}

Использование:

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

Objective-C

Заголовок:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Реализация:

#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
источник
1
Я считаю (не тестировал), что это может дать неточные результаты, если представленный контроллер представления запускает анимацию внутри реализаций viewDidLoad или viewWillAppear. Я думаю, что эти анимации будут запущены до того, как pushViewController: animated: returns - таким образом, обработчик завершения не будет вызван до тех пор, пока не завершатся вновь запущенные анимации.
Мэтт Х.
1
@MattH. Сделал пару тестов в этот вечер , и это выглядит , как при использовании pushViewController:animated:или popViewController:animated, то viewDidLoadи viewDidAppearвызовы происходят в последующих циклах runloop. Итак, у меня сложилось впечатление, что даже если эти методы вызывают анимацию, они не будут частью транзакции, представленной в примере кода. Это вас беспокоило? Потому что это решение сказочно простое.
LeffelMania 05
1
Оглядываясь назад на этот вопрос, я думаю, что в целом проблемы, упомянутые @MattH. и @LeffelMania действительно указывают на действительную проблему с этим решением - в конечном итоге предполагается, что транзакция будет завершена после завершения push, но структура не гарантирует такое поведение. didShowViewControllerТем не менее, это гарантированно, чем показан рассматриваемый контроллер представления . Хотя это решение фантастически просто, я бы поставил под сомнение его «надежность в будущем». Особенно с учетом изменений в просмотре обратных вызовов жизненного цикла, которые появились с ios7 / 8
Сэм
8
Это не работает надежно на устройствах iOS 9. См. Мои ответы или ответы @ par ниже для альтернативы
Майк Спрэг
1
@ZevEisenberg определенно. Мой ответ - код динозавра в этом мире ~~ 2 года
chrs 01
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() }
    }
}

РЕДАКТИРОВАТЬ: я добавил версию своего исходного ответа Swift 3. В этой версии я удалил пример совместной анимации, показанный в версии Swift 2, поскольку, похоже, это сбило с толку многих людей.

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
Есть ли какая-то конкретная причина, по которой вы говорите виртуальному компьютеру обновить строку состояния? Кажется, это нормально работает, передавая nilкак блок анимации.
Майк Спрэг
2
Это пример того, что вы можете сделать как параллельную анимацию (комментарий непосредственно над ним указывает, что это необязательно). Пройти nilтоже можно.
номинальное значение
1
@par, стоит ли вам быть более оборонительным и вызывать завершение, когда transitionCoordinatorноль?
Aurelien Porte
@AurelienPorte Это отличный улов, и я бы сказал, да, вам стоит. Обновлю ответ.
номинальная
1
@cbowns Я не уверен на 100% в этом, поскольку я не видел, чтобы это происходило, но если вы не видите, transitionCoordinatorто, вероятно, вы вызываете эту функцию слишком рано в жизненном цикле контроллера навигации. Подождите, по крайней мере, пока не viewWillAppear()будет вызван, прежде чем пытаться нажать на контроллер представления с анимацией.
номинальное значение
28

На основе ответа par (который был единственным, который работал с iOS9), но проще и с отсутствующим else (что могло привести к тому, что завершение никогда не было вызвано):

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()
        }
    }
}
Даниэль
источник
У меня не работает. Для меня transitionCoordinator равно нулю.
tcurdt
Работает для меня. Также этот вариант лучше, чем принятый, потому что завершение анимации не всегда совпадает с завершением push.
Антон Плебанович
Вам не хватает DispatchQueue.main.async для неанимированного дела. Контракт этого метода заключается в том, что обработчик завершения вызывается асинхронно, вы не должны нарушать это, потому что это может привести к незаметным ошибкам.
Вернер Альтевишер,
24

В настоящее время UINavigationControllerэто не поддерживается. Но есть то, UINavigationControllerDelegateчто можно использовать.

Легкий способ добиться этого - UINavigationControllerсоздать подкласс и добавить свойство блока завершения:

@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

Перед тем, как нажать новый контроллер представления, вам нужно будет установить блок завершения:

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

Этот новый подкласс может быть назначен в Interface Builder или использоваться программно следующим образом:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Клаас
источник
8
Добавление списка блоков завершения, сопоставленных с контроллерами представления, вероятно, сделает это наиболее полезным, а новый метод, возможно вызванный pushViewController:animated:completion:, сделает это элегантным решением.
Hyperbole
1
NB на 2018 год, это действительно просто ... stackoverflow.com/a/43017103/294884
Fattie 04
8

Вот версия Swift 4 с 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()
    }
}

На всякий случай это понадобится кому-то другому.

Франсуа Надо
источник
Если вы запустите простой тест, вы обнаружите, что блок завершения срабатывает до завершения анимации. Так что это, вероятно, не дает того, что многие ищут.
horseshoe7
7

Чтобы расширить ответ @Klaas (и в результате этого вопроса), я добавил блоки завершения непосредственно в метод 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

Используется следующим образом:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Сэм
источник
Блестяще. Большое спасибо
Петар
if... (self.pushedVC == viewController) {это неверно. Вам нужно проверить равенство между объектами, используя isEqual:, например,[self.pushedVC isEqual:viewController]
Эван Р.
@EvanR, что, вероятно, более технически правильно. вы видели ошибку при сравнении экземпляров другим способом?
Sam
@Sam не специально для этого примера (не реализовал его), но определенно для тестирования равенства с другими объектами - см. Документацию Apple по этому поводу: developer.apple.com/library/ios/documentation/General/… . Всегда ли в этом случае работает ваш метод сравнения?
Evan R
Я не видел, чтобы это не работало, иначе я бы изменил свой ответ. Насколько я знаю, iOS не делает ничего умного для воссоздания контроллеров представления, как это делает Android с действиями. но да, isEqualвероятно , было бы более технически правильным, если бы они когда-либо сделали.
Sam
5

Начиная с iOS 7.0, вы можете использовать UIViewControllerTransitionCoordinatorдля добавления блока завершения 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
источник
1
Это не совсем то же самое, что push, pop и т. Д. UINavigationController
Джон Уиллис,
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
источник
2

Чтобы добавить это поведение и сохранить возможность установки внешнего делегата, потребуется немного больше конвейеров.

Вот документированная реализация, которая поддерживает функции делегирования:

LBXCompletingNavigationController

nzeltzer
источник