Обробник завершення для UINavigationController “pushViewController: анімований”?


110

Я збираюся створити додаток, використовуючи UINavigationControllerдля подання наступних контролерів перегляду. З iOS5 є новий метод представлення UIViewControllers:

presentViewController:animated:completion:

Тепер я запитую мене, чому не існує обробник доробки UINavigationController? Є просто

pushViewController:animated:

Чи можливо створити власний обробник завершення, як новий presentViewController:animated:completion:?


2
не зовсім те саме, що обробник завершення, але viewDidAppear:animated:давайте виконувати код щоразу, коли ваш контролер подання відображається на екрані ( viewDidLoadлише коли перший раз завантажується ваш контролер подання даних)
Moxy

@Moxy, ти маєш на увазі-(void)viewDidAppear:(BOOL)animated
Джордж

2
на 2018 рік ... справді це просто так: stackoverflow.com/a/43017103/294884
Fattie

Відповіді:


139

Дивіться відповідь пар для іншого і більш сучасного рішення

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

Швидкий :

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

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
}

Ціль-С

Заголовок:

#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

1
Я вважаю (не перевіряв), що це може дати неточні результати, якщо представлений контролер перегляду запускає анімацію всередині його viewDidLoad або viewWillAppear. Я думаю, що ці анімації будуть запущені до pushViewController: animed: return - таким чином, обробник завершення не буде викликаний, поки щойно запущені анімації не закінчаться.
Метт Х.

1
@MattH. Пара зробила тестування сьогодні ввечері, і це виглядає, як при використанні pushViewController:animated:або popViewController:animated, viewDidLoadі viewDidAppearдзвінки відбуваються в наступних циклах запуску. Отже, моє враження полягає в тому, що навіть якщо ці методи не викликають анімацію, вони не будуть частиною транзакції, наданою в прикладі коду. Це було ваше занепокоєння? Тому що це рішення казково просто.
LeffelMania

1
Озираючись на це питання, я взагалі думаю, що проблеми, про які говорив @MattH. і @LeffelMania виділяють дійсну проблему з цим рішенням - це, зрештою, передбачає, що транзакція буде завершена після завершення натискання, але рамка не гарантує такої поведінки. Це гарантовано, ніж контролер перегляду, про який йде мова, didShowViewControllerхоча. Хоча це рішення фантастично просте, я б поставив під сумнів його "надійність у майбутньому". Особливо враховуючи зміни для перегляду зворотних дзвінків із життєвого циклу, які надійшли разом із ios7 / 8
Sam

8
Схоже, це не працює надійно на пристроях iOS 9. Дивіться відповіді моїх чи @ par нижче про альтернативу
Майк Спраг

1
@ZevEisenberg точно. Моя відповідь - код динозавра в цьому світі ~~ 2 роки
chrs

95

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

EDIT: Я додав свою оригінальну відповідь на Swift 3. У цій версії я видалив приклад спільної анімації, показаний у версії Swift 2, оскільки, здається, це заплутало багатьох людей.

Швидкий 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() }
    }
}

Швидкий 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 оновити його рядок стану? Здається, це добре спрацьовує nilяк анімаційний блок.
Майк Спраг

2
Це приклад того, що ви можете зробити як паралельну анімацію (коментар безпосередньо над нею вказує, що це необов’язково). Проїзд nil- цілком дійсна річ, яку теж потрібно зробити.
пар

1
@par, Якщо ви будете більш захисними і зателефонуєте завершенню, коли transitionCoordinatorнуль?
Aurelien Porte

@AurelienPorte Це чудовий улов, і я б сказав, так. Я оновлю відповідь.
пар

1
@cbowns Я не впевнений у цьому на 100%, оскільки не бачив цього, але якщо ви цього не бачите transitionCoordinator, швидше за все, ви викликаєте цю функцію занадто рано в життєвому циклі навігаційного контролера. Зачекайте принаймні до viewWillAppear()виклику, перш ніж намагатись натиснути контролер перегляду з анімацією.
par

28

Виходячи з відповіді par (яка була єдиною, яка працювала з iOS9), але простішою та з відсутнім іншим (що могло призвести до того, що завершення так і не було викликано):

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

Не працює для мене. Координатор переходу для мене нульовий.
tcurdt

Працює для мене. Також цей варіант краще прийняти, тому що завершення анімації не завжди те саме, що завершення натисканням.
Антон Плебанович

Вам не вистачає 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

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 не спеціально з цим прикладом (не реалізував його), але, безумовно, у тестуванні рівності з іншими об'єктами - див. Документи Apple щодо цього: developer.apple.com/library/ios/documentation/General/… . Чи завжди у вашому випадку працює метод порівняння?
Еван Р

Я не бачив, щоб це не працювало, або я змінив би свою відповідь. Наскільки я знаю, iOS не робить нічого розумного, щоб відтворити контролери перегляду, як це робить Android з діяльністю. але так, isEqualнапевно , це було б більш технічно правильним випадком, коли вони це робили.
Сем

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

1
Це не зовсім те саме, що UINavigationController push, pop тощо.
Джон Вілліс

3

Швидкий 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

Щоб додати цю поведінку і зберегти можливість встановлення зовнішнього делегата, потрібно трохи більше трубопроводів.

Ось задокументована реалізація, яка підтримує функціонал делегата:

LBXCompletingNavigationController

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.