UINavigationController "pushViewController : animated"에 대한 완료 처리기?


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

다른 최신 솔루션에 대한 par의 답변 참조

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
}

목표 -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

1
제시된 뷰 컨트롤러가 viewDidLoad 또는 viewWillAppear 구현 내에서 애니메이션을 트리거하면 이것이 부정확 한 결과를 제공 할 수 있다고 믿습니다 (테스트하지 않았습니다). 이러한 애니메이션은 pushViewController : animated :가 반환되기 전에 시작될 것이라고 생각합니다. 따라서 새로 트리거 된 애니메이션이 완료 될 때까지 완료 핸들러가 호출되지 않습니다.
Matt H.

1
@MattH. 오늘 저녁에 몇 번의 테스트를 수행했으며 pushViewController:animated:또는을 사용할 때처럼 보 였으며 popViewController:animated, viewDidLoadviewDidAppear호출은 후속 실행 루프주기에서 발생합니다. 그래서 내 인상은 이러한 메서드가 애니메이션을 호출하더라도 코드 예제에서 제공하는 트랜잭션의 일부가되지 않는다는 것입니다. 그게 당신의 걱정 이었나요? 이 솔루션은 엄청나게 간단하기 때문입니다.
LeffelMania

1
이 질문을 되돌아 보면 일반적으로 @MattH가 언급 한 우려 사항이 있다고 생각합니다. @LeffelMania는이 솔루션의 유효한 문제를 강조합니다. 궁극적으로 푸시가 완료된 후 트랜잭션이 완료 될 것이라고 가정하지만 프레임 워크는이 동작을 보장하지 않습니다. 문제의 뷰 컨트롤러가 표시된 것보다 보장 didShowViewController됩니다. 이 솔루션은 환상적으로 간단하지만 "미래 보장성"에 의문을 제기합니다. 특히 ios7 / 8
Sam

8
이것은 iOS 9 기기에서 안정적으로 작동하지 않는 것 같습니다. 대안은 아래 내 또는 @ 파의 답변을 참조하십시오
마이크 스프

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

편집 : 원래 답변의 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애니메이션 블록으로 잘 전달 되는 것 같습니다 .
Mike Sprague

2
병렬 애니메이션으로 수행 할 수있는 작업의 예입니다 (바로 위에있는 주석은 선택 사항임을 나타냄). 패스 nil도 완벽하게 타당한 일입니다.

1
@par, 당신은 더 방어 적이어야하고 완료 transitionCoordinator가 nil 일 때 호출해야합니까 ?
Aurelien Porte

@AurelienPorte 대단한 캐치이며 네, 그래야한다고 말하고 싶습니다. 답변을 업데이트하겠습니다.
par

1
@cbowns 나는 이것이 일어나는 것을 보지 못했기 때문에 이것에 대해 100 % 확신하지 못하지만, 만약 당신이 이것을 보지 못한다면 transitionCoordinator그것은 당신이 탐색 컨트롤러의 수명주기에서 너무 일찍이 함수를 호출했을 가능성이 있습니다. viewWillAppear()애니메이션이있는 뷰 컨트롤러를 푸시하기 전에가 호출 될 때까지 적어도 기다리십시오 .

28

를 기반으로 파의 대답은 하지만, 간단하고와 (유일한 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()
        }
    }
}

나를 위해 작동하지 않습니다. transitionCoordinator는 저에게 없습니다.
tcurdt 2016-06-15

나를 위해 작동합니다. 또한 애니메이션 완료가 항상 푸시 완료와 같지는 않기 때문에이 방법이 허용되는 것보다 낫습니다.
Anton Plebanovich 2016 년

애니메이션이 아닌 케이스에 대한 DispatchQueue.main.async가 누락되었습니다. 이 메소드의 계약은 완료 핸들러가 비동기 적으로 호출된다는 것입니다. 미묘한 버그가 발생할 수 있으므로이를 위반해서는 안됩니다.
Werner Altewischer

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 2013

1
2018 년 NB는 정말
이거예요

8

Pop이있는 Swift 4 버전입니다.

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의 답변을 확장하기 위해 ( 그리고이 질문 의 결과로 ) 푸시 메서드에 직접 완료 블록을 추가했습니다.

@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]
에반 R

@EvanR 아마도 기술적으로 더 정확할 것입니다. 인스턴스를 다른 방식으로 비교하는 데 오류가 있습니까?
Sam

@Sam은이 예제를 구체적으로 설명하지는 않았지만 (구현하지 않았 음) 다른 객체와의 동등성을 테스트 할 때 확실히 있습니다. developer.apple.com/library/ios/documentation/General/… 에 대한 Apple의 문서를 참조하십시오 . 이 경우 비교 방법이 항상 작동합니까?
Evan R

나는 그것이 작동하지 않는 것을 보지 못했거나 내 대답을 변경했을 것입니다. 내가 아는 한 iOS는 Android가 활동과 함께하는 것처럼 뷰 컨트롤러를 다시 만드는 데 영리한 작업을 수행하지 않습니다. 그러나 예, isEqual그들이 한 경우 기술적으로 더 정확할 것입니다.
Sam

5

iOS 7.0부터 UIViewControllerTransitionCoordinator푸시 완료 블록을 추가하는 데 사용할 수 있습니다.

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 푸시, 팝,로 꽤 같은 일이 아니다
존 윌리스

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.