ตัวจัดการความสมบูรณ์สำหรับ 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

คำตอบ:


140

ดูคำตอบของพาร์สำหรับโซลูชันอื่นและล่าสุด

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: return - ดังนั้นตัวจัดการที่สมบูรณ์จะไม่ถูกเรียกจนกว่าภาพเคลื่อนไหวที่เรียกใหม่จะเสร็จสิ้น
Matt H.

1
@MattH. ทำคู่ทดสอบช่วงเย็นวันนี้และดูเหมือนว่าเมื่อมีการใช้pushViewController:animated:หรือpopViewController:animatedการviewDidLoadและviewDidAppearสายเกิดขึ้นในรอบ runloop ภายหลัง ดังนั้นความประทับใจของฉันคือแม้ว่าวิธีการเหล่านั้นจะเรียกใช้ภาพเคลื่อนไหว แต่ก็จะไม่เป็นส่วนหนึ่งของธุรกรรมที่ให้ไว้ในตัวอย่างโค้ด นั่นเป็นความกังวลของคุณหรือไม่? เนื่องจากวิธีนี้ง่ายมาก
LeffelMania

1
เมื่อมองย้อนกลับไปที่คำถามนี้ฉันคิดว่าโดยทั่วไปแล้วข้อกังวลที่ @MattH กล่าวถึง และ @LeffelMania จะเน้นถึงปัญหาที่ถูกต้องกับโซลูชันนี้ - ในที่สุดก็ถือว่าธุรกรรมจะเสร็จสมบูรณ์หลังจากการพุชเสร็จสมบูรณ์ แต่กรอบงานไม่รับประกันพฤติกรรมนี้ จะรับประกันนอกเหนือจากตัวควบคุมมุมมองในคำถามที่ปรากฏอยู่ในdidShowViewControllerแต่ แม้ว่าวิธีนี้จะง่ายอย่างน่าอัศจรรย์ แต่ฉันก็ตั้งคำถามว่า "การพิสูจน์อนาคต" โดยเฉพาะอย่างยิ่งเมื่อได้รับการเปลี่ยนแปลงเพื่อดูการเรียกกลับของวงจรชีวิตที่มาพร้อมกับ ios7 / 8
Sam

8
ดูเหมือนว่าจะทำงานได้ไม่น่าเชื่อถือบนอุปกรณ์ iOS 9 ดูคำตอบของฉันหรือ @ par ด้านล่างสำหรับทางเลือกอื่น
Mike Sprague

1
@ZevEisenberg แน่นอน. คำตอบของฉันคือรหัสไดโนเสาร์ในโลกนี้ ~~ อายุ 2 ขวบ
chrs

97

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

แก้ไข: ฉันได้เพิ่มคำตอบเดิมของเวอร์ชัน Swift 3 แล้ว ในเวอร์ชันนี้ฉันได้ลบตัวอย่างแอนิเมชั่นร่วมที่แสดงในเวอร์ชัน Swift 2 เนื่องจากดูเหมือนว่าจะทำให้หลาย ๆ คนสับสน

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
มีเหตุผลเฉพาะไหมที่คุณบอกให้ vc อัปเดตแถบสถานะ ดูเหมือนว่าจะทำงานได้ดีnilในฐานะบล็อกภาพเคลื่อนไหว
Mike Sprague

2
เป็นตัวอย่างของสิ่งที่คุณอาจทำเป็นภาพเคลื่อนไหวคู่ขนาน (ความคิดเห็นด้านบนแสดงว่าไม่บังคับ) การผ่านnilก็เป็นสิ่งที่ถูกต้องเช่นกัน
พาร์

1
@par คุณควรจะป้องกันมากกว่านี้และเรียกการทำให้เสร็จเมื่อtransitionCoordinatorเป็นศูนย์?
Aurelien Porte

@AurelienPorte นั่นเป็นสิ่งที่ดีมากและฉันก็บอกว่าใช่คุณควร ฉันจะอัปเดตคำตอบ
พาร์

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

ไม่ได้ผลสำหรับฉัน การเปลี่ยนแปลง Coordinator เป็นศูนย์สำหรับฉัน
tcurdt

ใช้ได้ผลสำหรับฉัน นอกจากนี้อันนี้ยังดีกว่าที่ได้รับการยอมรับเนื่องจากการสร้างภาพเคลื่อนไหวไม่เหมือนกับการกดเสร็จเสมอไป
Anton Plebanovich

คุณไม่มี 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

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

ในกรณีที่มีคนต้องการสิ่งนี้


หากคุณทำการทดสอบง่ายๆเกี่ยวกับเรื่องนี้คุณจะพบว่าบล็อกที่สมบูรณ์จะเริ่มทำงานก่อนที่ภาพเคลื่อนไหวจะเสร็จสิ้น ดังนั้นสิ่งนี้อาจไม่ได้ให้สิ่งที่หลายคนกำลังมองหา
เกือกม้า 7

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

ยอดเยี่ยม. ขอบคุณมาก
Petar

if... (self.pushedVC == viewController) {ไม่ถูกต้อง คุณต้องทดสอบความเท่าเทียมระหว่างวัตถุโดยใช้isEqual:เช่น[self.pushedVC isEqual:viewController]
Evan R

@EvanR นั่นน่าจะถูกต้องทางเทคนิคมากกว่าใช่ คุณเคยเห็นข้อผิดพลาดในการเปรียบเทียบอินสแตนซ์ด้วยวิธีอื่นหรือไม่?
สาม

@ Sam ไม่เฉพาะกับตัวอย่างนี้ (ไม่ได้ใช้มัน) แต่แน่นอนในการทดสอบความเท่าเทียมกันกับคนอื่น ๆ ได้เห็นวัตถุเอกสารแอปเปิ้ลเกี่ยวกับเรื่องนี้: developer.apple.com/library/ios/documentation/General/... วิธีการเปรียบเทียบของคุณใช้ได้ผลเสมอในกรณีนี้หรือไม่?
Evan R

ฉันไม่เห็นว่ามันไม่ได้ผลหรือฉันจะเปลี่ยนคำตอบ เท่าที่ฉันรู้ว่า iOS ไม่ได้ทำอะไรที่ฉลาดในการสร้างตัวควบคุมมุมมองใหม่เหมือนที่ Android ทำกับกิจกรรม แต่ใช่isEqualน่าจะถูกต้องทางเทคนิคมากกว่าในกรณีที่พวกเขาเคยทำ
สาม

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 push, pop และอื่น ๆ
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

ต้องใช้เวลาอีกเล็กน้อยในการเพิ่มลักษณะการทำงานนี้และรักษาความสามารถในการตั้งค่าผู้รับมอบสิทธิ์ภายนอก

นี่คือเอกสารการใช้งานที่รักษาการทำงานของผู้ร่วมประชุม:

LBXCompletingNavigationController

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.