Trình xử lý hoàn thành cho UINavigationController “pushViewController: animation”?


110

Tôi sắp tạo một ứng dụng bằng cách sử dụng a UINavigationControllerđể trình bày bộ điều khiển chế độ xem tiếp theo. Với iOS5 có một phương pháp mới để trình bày UIViewControllers:

presentViewController:animated:completion:

Bây giờ tôi hỏi tôi tại sao không có một trình xử lý hoàn thành cho UINavigationController? Chỉ có

pushViewController:animated:

Có thể tạo trình xử lý hoàn thành của riêng tôi như mới presentViewController:animated:completion:không?


2
không chính xác những điều tương tự như một trình xử lý hoàn thành nhưng viewDidAppear:animated:cho phép của bạn thực thi mã mỗi lần xem điều khiển của bạn sẽ xuất hiện trên màn hình ( viewDidLoadchỉ lần đầu tiên điều khiển xem của bạn được nạp)
MOXY

@Moxy, ý bạn là-(void)viewDidAppear:(BOOL)animated
George

2
cho 2018 ... thực sự nó chỉ này: stackoverflow.com/a/43017103/294884
Fattie

Câu trả lời:


139

Xem câu trả lời của par cho một giải pháp khác và cập nhật hơn

UINavigationControllerhoạt ảnh được chạy với CoreAnimation, vì vậy sẽ có ý nghĩa nếu đóng gói mã bên trong CATransactionvà do đó đặt một khối hoàn thành.

Swift :

Để nhanh chóng, tôi khuyên bạn nên tạo một tiện ích mở rộng như vậy

extension UINavigationController {

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

}

Sử dụng:

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

Objective-C

Tiêu đề:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Thực hiện:

#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
Tôi tin rằng (chưa thử nghiệm) rằng điều này có thể cung cấp kết quả không chính xác nếu bộ điều khiển chế độ xem được trình bày kích hoạt hoạt ảnh bên trong triển khai viewDidLoad hoặc viewWillAppear. Tôi nghĩ những hoạt ảnh đó sẽ được bắt đầu trước khi pushViewController: animation: return - do đó, trình xử lý hoàn thành sẽ không được gọi cho đến khi các hoạt ảnh mới được kích hoạt kết thúc.
Matt H.

1
@MattH. Đã thực hiện một vài thử nghiệm vào tối nay và có vẻ như khi sử dụng pushViewController:animated:hoặc popViewController:animated, viewDidLoadviewDidAppearcác cuộc gọi xảy ra trong các chu kỳ runloop tiếp theo. Vì vậy, ấn tượng của tôi là ngay cả khi những phương thức đó gọi hoạt ảnh, chúng sẽ không phải là một phần của giao dịch được cung cấp trong ví dụ mã. Đó có phải là mối quan tâm của bạn? Bởi vì giải pháp này rất đơn giản.
LeffelMania

1
Nhìn lại câu hỏi này, tôi nghĩ về tổng thể những lo ngại mà @MattH đã đề cập. và @LeffelMania làm nổi bật một vấn đề hợp lệ với giải pháp này - cuối cùng nó giả định giao dịch sẽ được hoàn thành sau khi quá trình đẩy hoàn tất, nhưng khuôn khổ không đảm bảo hành vi này. Nó được đảm bảo hơn so với bộ điều khiển chế độ xem đang đề cập được hiển thị trong didShowViewControllermặc dù. Mặc dù giải pháp này đơn giản đến kinh ngạc, nhưng tôi sẽ đặt câu hỏi về "khả năng chống lại tương lai" của nó. Đặc biệt là với những thay đổi để xem các lệnh gọi lại vòng đời đi kèm với ios7 / 8
Sam

8
Điều này dường như không hoạt động đáng tin cậy trên các thiết bị iOS 9. Xem câu trả lời mệnh của tôi hoặc @ dưới đây cho một sự thay thế
Mike Sprague

1
@ZevEisenberg chắc chắn. Câu trả lời của tôi là mật mã khủng long trong thế giới này ~~ 2 tuổi
chrs

95

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

CHỈNH SỬA: Tôi đã thêm phiên bản Swift 3 cho câu trả lời ban đầu của mình. Trong phiên bản này, tôi đã loại bỏ co-animation ví dụ được hiển thị trong phiên bản Swift 2 vì nó có vẻ như đã khiến nhiều người nhầm lẫn.

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
Có lý do cụ thể nào khiến bạn yêu cầu vc cập nhật thanh trạng thái của nó không? Điều này dường như hoạt động tốt khi chuyển nilvào dưới dạng khối hoạt ảnh.
Mike Sprague

2
Đó là một ví dụ về điều gì đó bạn có thể làm dưới dạng hoạt ảnh song song (nhận xét ngay phía trên nó cho biết đó là tùy chọn). Vượt qua nilcũng là một điều hoàn toàn hợp lệ.
par

1
@par, Bạn có nên phòng thủ hơn và gọi hoàn thành khi giá trị transitionCoordinatorlà con số không?
Aurelien Porte

@AurelienPorte Đó là một sản phẩm tuyệt vời và tôi muốn nói là có, bạn nên làm như vậy. Tôi sẽ cập nhật câu trả lời.
par

1
@cbowns Tôi không chắc chắn 100% về điều này vì tôi chưa thấy điều này xảy ra, nhưng nếu bạn không thấy transitionCoordinatorthì có khả năng bạn đang gọi hàm này quá sớm trong vòng đời của bộ điều khiển điều hướng. Chờ ít nhất cho đến khi viewWillAppear()được gọi trước khi cố gắng đẩy bộ điều khiển chế độ xem có hoạt ảnh.
par

28

Dựa trên câu trả lời của mệnh (là câu duy nhất hoạt động với iOS9), nhưng đơn giản hơn và thiếu một câu khác (có thể dẫn đến việc hoàn thành không bao giờ được gọi):

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

Không làm việc cho tôi. Điều phối viên chuyển đổi là con số không đối với tôi.
tcurdt

Làm việc cho tôi. Ngoài ra, cái này tốt hơn nên được chấp nhận một cái vì hoàn thành hoạt ảnh không phải lúc nào cũng giống như hoàn thành đẩy.
Anton Plebanovich

Bạn đang thiếu DispatchQueue.main.async cho trường hợp không hoạt hình. Hợp đồng của phương pháp này là trình xử lý hoàn thành được gọi là không đồng bộ, bạn không nên vi phạm điều này vì nó có thể dẫn đến các lỗi nhỏ.
Werner Altewischer

24

Hiện tại UINavigationControllerkhông hỗ trợ điều này. Nhưng có những thứ UINavigationControllerDelegatemà bạn có thể sử dụng.

Một cách dễ dàng để thực hiện điều này là bằng cách phân lớp con UINavigationControllervà thêm thuộc tính khối hoàn thành:

@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

Trước khi đẩy bộ điều khiển chế độ xem mới, bạn sẽ phải đặt khối hoàn thành:

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

Lớp con mới này có thể được chỉ định trong Trình tạo giao diện hoặc được sử dụng theo chương trình như thế này:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];

8
Thêm danh sách các khối hoàn thành được ánh xạ để xem bộ điều khiển có lẽ sẽ làm cho điều này hữu ích nhất và một phương pháp mới, có lẽ được gọi là pushViewController:animated:completion:sẽ làm cho điều này trở thành một giải pháp thanh lịch.
Hyperbole

1
NB cho năm 2018 nó thực sự chỉ này ... stackoverflow.com/a/43017103/294884
Fattie

8

Đây là phiên bản Swift 4 với 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()
    }
}

Đề phòng người khác cần cái này.


Nếu bạn chạy một thử nghiệm đơn giản về điều này, bạn sẽ thấy rằng khối hoàn thành sẽ kích hoạt trước khi hoạt ảnh kết thúc. Vì vậy, điều này có thể không cung cấp những gì nhiều người đang tìm kiếm.
móng ngựa

7

Để mở rộng câu trả lời của @Klaas (và là kết quả của câu hỏi này ), tôi đã thêm các khối hoàn thành trực tiếp vào phương thức 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

Để được sử dụng như sau:

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

Xuất sắc. Cảm ơn rất nhiều
Petar

if... (self.pushedVC == viewController) {là không chính xác. Bạn cần phải bình đẳng giữa các đối tượng thử nghiệm bằng cách sử dụng isEqual:, ví dụ,[self.pushedVC isEqual:viewController]
Evan R

@EvanR có lẽ đúng về mặt kỹ thuật hơn. bạn có thấy lỗi khi so sánh các trường hợp theo cách khác không?
Sam

@Sam không cụ thể với ví dụ này (không triển khai nó) nhưng chắc chắn trong việc kiểm tra tính bình đẳng với các đối tượng khác — hãy xem tài liệu của Apple về điều này: developer.apple.com/library/ios/documentation/General/… . Phương pháp so sánh của bạn có luôn hoạt động trong trường hợp này không?
Evan R,

Tôi đã không thấy nó không hoạt động hoặc tôi sẽ thay đổi câu trả lời của mình. Theo như tôi biết, iOS không làm bất cứ điều gì thông minh để tạo lại bộ điều khiển chế độ xem như Android làm với các hoạt động. nhưng có, isEqualcó lẽ sẽ đúng về mặt kỹ thuật hơn nếu họ đã từng làm.
Sam

5

Kể từ iOS 7.0, bạn có thể sử dụng UIViewControllerTransitionCoordinatorđể thêm khối hoàn thành đẩy:

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
Đây không phải là hoàn toàn tương tự như UINavigationController push, pop, vv
Jon Willis

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

2

Phải mất thêm một chút thao tác để thêm hành vi này và giữ lại khả năng thiết lập một đại biểu bên ngoài.

Đây là một triển khai được lập thành văn bản để duy trì chức năng của đại biểu:

LBXCompletingNavigationController

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.