Блок завершения для popViewController

115

При отказе от использования контроллера модального представления dismissViewControllerесть возможность предоставить блок завершения. Есть ли аналогичный для popViewController?

Аргумент завершения очень удобен. Например, я могу использовать его, чтобы отложить удаление строки из представления таблицы до тех пор, пока модальное окно не выключится, позволяя пользователю увидеть анимацию строки. Когда я возвращаюсь из выталкиваемого контроллера представления, мне нужна такая же возможность.

Я попытался разместить popViewControllerв UIViewблоке анимации, где у меня есть доступ к блоку завершения. Однако это приводит к нежелательным побочным эффектам в отображаемом представлении.

Если такого метода нет, каковы обходные пути?

Бен Паккард
источник
stackoverflow.com/a/33767837/2774520 я думаю, что этот способ самый родной
Алексей Нежиборец
3
Для 2018 года это очень просто и стандартно: stackoverflow.com/a/43017103/294884
Fattie

Ответы:

201

Я знаю, что ответ был принят более двух лет назад, однако это неполный ответ.

Нет возможности делать то, что вы хотите, из коробки

Это технически правильно, потому что UINavigationControllerAPI не предлагает для этого никаких вариантов. Однако с помощью структуры CoreAnimation можно добавить блок завершения к базовой анимации:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

Блок завершения будет вызван, как только анимация, используемая пользователем, popViewControllerAnimated:закончится. Эта функция доступна с iOS 4.

Йорис Клюверс
источник
5
Я поместил это в расширение UINavigationController в Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur
1
Кажется, у меня не работает, когда я выполняю завершениеHandler на dismissViewController, представление, которое его представляло, является частью иерархии представлений. Когда я делаю то же самое с CATransaction, я получаю предупреждение о том, что представление не является частью иерархии представлений.
moger777
1
Хорошо, похоже, ваши работы, если поменять местами начало и завершение блока. Извините за голосование против, но переполнение стека не позволяет мне измениться :(
moger777
7
Да, казалось, что это будет круто, но похоже, что это не работает (по крайней мере, на iOS 8). Немедленно вызывается блок завершения. Вероятно, из-за смеси базовой анимации с анимацией в стиле UIView.
stuckj
5
ЭТО НЕ
РАБОТАЕТ
52

Для версии iOS9 SWIFT - работает как шарм (для более ранних версий не тестировался). На основании этого ответа

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()
        }
    }
}
HotJard
источник
Не будет работать, если не анимирован, должен завершить выполнение при следующем цикле выполнения, чтобы сделать это правильно.
rshev 06
@rshev почему на следующем цикле выполнения?
Бен Синклер,
@ Энди, насколько я помню, экспериментировал с этим, на тот момент что-то еще не распространялось. Попробуйте поэкспериментировать с этим, я люблю слышать, как это работает для вас.
rshev
@rshev Я думаю, что у меня было так же раньше, я должен перепроверить. Текущие тесты проходят нормально.
Бен Синклер
1
@LanceSamaria Я предлагаю использовать viewDidDisappear. Проверьте, доступна ли навигационная панель, если нет - она ​​не отображается на навигационной панели, поэтому была открыта. if (self.navigationController == nil) {инициируйте ваше действие}
HotJard
33

Я сделал Swiftверсию с расширениями с ответом @JorisKluivers .

Это вызовет завершение закрытия после того, как анимация будет выполнена для обоих pushи 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()
    }
}
Arbitur
источник
Для меня в iOS 8.4, написанной на ObjC, блок срабатывает на половине анимации. Действительно ли это срабатывает в нужный момент, если написано на Swift (8.4)?
Джулиан Ф. Вайнерт
Блок завершения @Arbitur действительно вызывается после вызова popViewControlleror pushViewController, но если вы проверите, что такое topViewController сразу после этого, вы заметите, что он все еще старый, точно так же, как popили pushникогда не было ...
Богдан Разван
@BogdanRazvan что потом? Вызывается ли ваше завершение закрытия после завершения анимации?
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

Я была такая же проблема. И поскольку мне приходилось использовать его несколько раз и в цепочках блоков завершения, я создал это универсальное решение в подклассе UINavigationController:

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

Если предположить,

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

и

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

и

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}
Jos Jong
источник
1
Мне очень нравится это решение, я собираюсь попробовать его с категорией и связанным объектом.
spstanley
@spstanley, вам нужно опубликовать этот модуль :)
k06a
Быстрая версия -> stackoverflow.com/a/60090678/4010725
УИЛЛ К.
15

Нет возможности делать то, что вы хотите, из коробки. то есть не существует метода с блоком завершения для извлечения контроллера представления из стека навигации.

Я бы добавил логику viewDidAppear. Это будет вызвано, когда представление перестанет отображаться на экране. Он будет вызываться для всех различных сценариев появления контроллера представления, но это должно быть нормально.

Или вы можете использовать этот UINavigationControllerDelegateметод, navigationController:didShowViewController:animated:чтобы сделать то же самое. Это вызывается, когда контроллер навигации завершает нажатие или выталкивание контроллера представления.

Mattjgalloway
источник
Я пытался это сделать. Я хранил массив «индексов удаленных строк» ​​и всякий раз, когда появлялось представление, проверял, нужно ли что-нибудь удалить. Он быстро стал громоздким, но я мог бы дать ему еще один шанс. Интересно, почему Apple предоставляет это для одного перехода, а для другого нет?
Бен Паккард,
1
Это только самое новое на dismissViewController. Может, до этого дойдет popViewController. Подать радар :-).
mattjgalloway
А если серьезно, подайте радар. У него больше шансов попасть, если люди попросят об этом.
mattjgalloway
1
Это подходящее место, чтобы попросить об этом. Существует вариант классификации как «Feature».
mattjgalloway
3
Это не совсем правильный ответ. Хотя вы не можете установить блок нового стиля, как on -dismissViewController:animated:completionBlock:, но вы можете получить анимацию через делегата контроллера навигации. После завершения анимации -navigationController:didShowViewController:animated:будет вызван делегат, и вы сможете делать все, что вам нужно, прямо сейчас.
Джейсон Коко,
13

Правильная работа с анимацией или без нее, а также включает 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)
  }
}
rshev
источник
Какая-либо конкретная причина, по которой вы вызываете completion()асинхронный режим ?
Левиафан
1
когда анимация с координатором completionникогда не выполняется в одном цикле выполнения. это гарантирует, что completionникогда не будет запущен один и тот же цикл выполнения без анимации. Лучше не допускать такой непоследовательности.
rshev
11

На основе ответа @ HotJard, когда все, что вам нужно, это всего лишь пара строк кода. Быстро и просто.

Swift 4 :

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}
Vitalii
источник
6

На 2018 год ...

если у тебя есть это ...

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

и вы хотите добавить доработку ...

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

это так просто.

Подсказка ...

То же самое и для удобных popToViewController звонка.

Как правило, у вас есть стэк из миллиарда экранов. Когда, наконец, закончите, вы полностью вернетесь к своему «базовому» экрану, а затем, наконец, запустите приложение.

Итак, на «базовом» экране, чтобы вернуться «полностью назад», 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()
}
Fattie
источник
5

Блок завершения вызывается после вызова метода viewDidDisappear в представленном контроллере представления, поэтому размещение кода в методе viewDidDisappear контроллера всплывающего представления должно работать так же, как блок завершения.

Rdelmar
источник
Конечно, за исключением случаев, когда представление исчезает по какой-либо другой причине.
Бен Паккард,
1
@BenPackard, да, и то же самое верно для помещения его в viewDidAppear в ответе, который вы приняли.
rdelmar
5

Ответ Swift 3, благодаря этому ответу: 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 с необязательным параметром viewController для перехода к конкретному.

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()
        }
    }
}
TejAces
источник
Принятый ответ, похоже, работает в моей среде разработки со всеми эмуляторами / устройствами, которые у меня есть, но я все еще получаю сообщения об ошибках от производственных пользователей. Не уверен, что это решит проблему с производством, но позвольте мне проголосовать за него, чтобы кто-то мог попробовать, если получит ту же проблему из принятого ответа.
Шон
4

На основе этого ответа исправлена версия Swift 4 .

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()
        }
    }
}
d4Rk
источник
2

2020 Swift 5.1 способ

Это решение гарантирует, что завершение будет выполнено после полного завершения popViewController. Вы можете проверить это, выполнив еще одну операцию в NavigationController в завершении: во всех других решениях выше UINavigationController все еще занят операцией popViewController и не отвечает.

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

Для полноты картины я подготовил категорию Objective-C:

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

Я добился именно этого с точностью с помощью блока. Я хотел, чтобы мой полученный контроллер результатов отображал строку, добавленную модальным представлением, только после того, как она полностью покинула экран, чтобы пользователь мог видеть происходящие изменения. При подготовке к переходу, который отвечает за отображение контроллера модального представления, я устанавливаю блок, который хочу выполнить, когда модальное окно исчезнет. И в контроллере модального представления я переопределяю viewDidDissapear, а затем вызываю блок. Я просто начинаю обновления, когда появится модальное окно, и заканчиваю обновления, когда он исчезает, но это потому, что я использую NSFetchedResultsController, однако внутри блока вы можете делать все, что захотите.

-(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
malhal
источник
1

Используйте следующее расширение в своем коде: (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()
    }
}
Ригоберто Саенс Имбакуан
источник