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


113

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

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

Я спробував розміщення popViewControllerв UIViewблоці анімації, де у мене є доступ до блоку завершення. Однак це призводить до появи небажаних побічних ефектів для зору, на який випливає.

Якщо такого методу немає, які деякі шляхи вирішення?


stackoverflow.com/a/33767837/2774520 Я думаю, що цей спосіб є
найріднішим


3
Для 2018 року це дуже просто та стандартно: stackoverflow.com/a/43017103/294884
Fattie

Відповіді:


199

Я знаю, що відповідь була прийнята більше двох років тому, проте ця відповідь є неповною.

Немає можливості зробити те, що ви хочете, поза межами коробки

Це технічно правильно, оскільки UINavigationControllerAPI не пропонує жодних варіантів для цього. Однак за допомогою основи CoreAnimation можна додати блок завершення до базової анімації:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

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


5
Я поклав це на розширення UINavigationController у Свіфті:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Арбітур

1
Здається, це не працює для мене, коли я завершуюHandler на dismissViewController, погляд, який представляє його, є частиною ієрархії перегляду. Коли я роблю те саме з CATransaction, я отримую попередження про те, що представлення не є частиною ієрархії перегляду.
moger777

1
Гаразд, це виглядає як ваші роботи, якщо ви перевернете блок початку та завершення. Вибачте за голосування за
відмову,

7
Так, це здавалося б дивним, але, здається, це не працює (принаймні, на iOS 8). Блок завершення викликається негайно. Ймовірно, через суміш основних анімацій з анімацією стилю UIView.
stuckj

5
ЦЕ НЕ ПРАЦЮЄ
тривало

51

Для версії 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()
        }
    }
}

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

@rshev чому на наступному runloop?
Бен Сінклер

@Анді, наскільки я пам'ятаю, експериментуючи з цим, щось ще не було розповсюджено в той момент. Спробуйте експериментувати з цим, любите слухати, як це працює на вас.
ршев

@rshev Я думаю, що в мене було так само, як і раніше, я повинен перевірити двічі. Поточні тести працюють нормально.
Бен Сінклер

1
@LanceSamaria Я пропоную використовувати viewDidDisappear. Перевірте, чи доступний навбар, якщо ні - він не відображається в навібарі, тому він з’явився. якщо (self.navigationController == нуль) {запустити свою дію}
HotJard

32

Я зробив 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()
    }
}

Для мене в iOS 8.4, написаному в ObjC, блок проходить на півдорозі анімації. Чи справді це спрацьовує в потрібний момент, якщо написано у Swift (8.4)?
Julian F. Weinert

@Arbitur блок завершення дійсно викликається після виклику popViewControllerабо 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;
}

1
Мені дуже подобається це рішення, я збираюся спробувати його з категорією та асоційованим об’єктом.
спстанлі

@spstanley вам потрібно опублікувати цей
струк

Швидка версія -> stackoverflow.com/a/60090678/4010725
WILL K.

15

Немає можливості зробити те, що ви хочете, поза межами коробки. тобто не існує методу з блоком завершення для вискакування контролера подання зі стека nav.

Що я б робив, це вкласти логіку viewDidAppear. Це буде викликано, коли перегляд закінчиться, що надходить на екран. Це буде викликано для всіх різних сценаріїв, що з'являються контролером перегляду, але це повинно бути добре.

Або ви можете скористатися UINavigationControllerDelegateметодом, navigationController:didShowViewController:animated:щоб зробити подібну дію. Це називається, коли навігаційний контролер закінчив натискати або вискакувати контролер перегляду.


Я спробував це. Я зберігав масив "видалених індексів рядків" і кожного разу, коли з'являється представлення, перевіряючи, чи потрібно щось видалити. Це швидко стало непростим, але я можу дати йому ще один вистріл. Цікаво, чому Apple надає його для одного переходу, а не іншого?
Бен Пакард

1
Це лише дуже нове на dismissViewController. Можливо, воно і прийде popViewController. Подайте радар :-).
mattjgalloway

Серйозно, хоча, подайте РЛС. Це більше шансів зробити це, якщо люди просять про це.
mattjgalloway

1
Це правильне місце, щоб попросити його. Існує можливість класифікації "Feature".
mattjgalloway

3
Ця відповідь не зовсім коректна. Хоча ви не можете встановити блок нового стилю, як увімкнутий -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)
  }
}

Якась конкретна причина, чому ви викликаєте completion()асинхронізу?
Левіафан

1
коли анімація з координатором completionніколи не виконується на одній пробіжці. ця гарантія completionніколи не працює на одній пробіжці, коли не анімація. краще не мати такого роду непослідовність.
ршев

11

На основі відповіді @ HotJard, коли все, що вам потрібно, - це лише пара рядків коду. Швидко і просто

Швидкий 4 :

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}

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

5

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


Звичайно - крім того, вам доведеться впоратися зі всіма випадками, коли погляд зникає з якоїсь іншої причини.
Бен Пакард

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

Схоже, прийнята відповідь працює в моєму середовищі розробників із усіма емуляторами / пристроями, які у мене є, але я все одно отримую повідомлення про помилки від виробничих користувачів. Не впевнений, чи це вирішить виробничу проблему, але дозвольте мені здійснити це, щоб хтось міг спробувати це, якщо отримати те саме питання із прийнятої відповіді.
Шон

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


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

Я досяг цього саме з точністю, використовуючи блок. Я хотів, щоб мій контролер результатів показав рядок, який додав модальний вигляд, лише після того, як він повністю вийшов з екрана, щоб користувач міг бачити зміни, що відбуваються. Готуючись до segue, який відповідає за показ контролера модального перегляду, я встановлюю блок, який я хочу виконати, коли модаль зникає. І в контролері модального перегляду я переосмислюю 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

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