IOS: Есть ли способ предотвратить одновременное нажатие или всплытие двух контроллеров представления?

Единственное решение, которое я видел, было ответом на вопрос stackoverflow. Я разместил ссылку ниже. Ответ, который я имею в виду, - это пятый. Похоже, что некоторые пользователи имеют некоторые проблемы с решением. Я не знаю, есть ли другая категория, чтобы предотвратить одновременное нажатие двух контроллеров. Любые советы или предложения приветствуются.

#import "UINavigationController+Consistent.h"
#import <objc/runtime.h>
/// This char is used to add storage for the isPushingViewController property.
 static char const * const ObjectTagKey = "ObjectTag";

 @interface UINavigationController ()
 @property (readwrite,getter = isViewTransitionInProgress) BOOL viewTransitionInProgress;

 @end

@implementation UINavigationController (Consistent)

- (void)setViewTransitionInProgress:(BOOL)property {
NSNumber *number = [NSNumber numberWithBool:property];
objc_setAssociatedObject(self, ObjectTagKey, number , OBJC_ASSOCIATION_RETAIN);
}


- (BOOL)isViewTransitionInProgress {
NSNumber *number = objc_getAssociatedObject(self, ObjectTagKey);

return [number boolValue];
}


 #pragma mark - Intercept Pop, Push, PopToRootVC
 /// @name Intercept Pop, Push, PopToRootVC

 - (NSArray *)safePopToRootViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
    self.viewTransitionInProgress = YES;
}
//-- This is not a recursion, due to method swizzling the call below calls the original  method.
return [self safePopToRootViewControllerAnimated:animated];

 }


  - (NSArray *)safePopToViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
    self.viewTransitionInProgress = YES;
   }
//-- This is not a recursion, due to method swizzling the call below calls the original  method.
return [self safePopToViewController:viewController animated:animated];
   }


 - (UIViewController *)safePopViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
    self.viewTransitionInProgress = YES;
}
//-- This is not a recursion, due to method swizzling the call below calls the original  method.
return [self safePopViewControllerAnimated:animated];
 }



  - (void)safePushViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.delegate = self;
//-- If we are already pushing a view controller, we dont push another one.
if (self.isViewTransitionInProgress == NO) {
    //-- This is not a recursion, due to method swizzling the call below calls the original  method.
    [self safePushViewController:viewController animated:animated];
    if (animated) {
        self.viewTransitionInProgress = YES;
    }
    }
    }


// This is confirmed to be App Store safe.
// If you feel uncomfortable to use Private API, you could also use the delegate method navigationController:didShowViewController:animated:.
- (void)safeDidShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
//-- This is not a recursion. Due to method swizzling this is calling the original method.
[self safeDidShowViewController:viewController animated:animated];
self.viewTransitionInProgress = NO;
 }


// If the user doesnt complete the swipe-to-go-back gesture, we need to intercept it and set the flag to NO again.
 - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
id<UIViewControllerTransitionCoordinator> tc = navigationController.topViewController.transitionCoordinator;
[tc notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    self.viewTransitionInProgress = NO;
    //--Reenable swipe back gesture.
    self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)viewController;
    [self.interactivePopGestureRecognizer setEnabled:YES];
}];
//-- Method swizzling wont work in the case of a delegate so:
//-- forward this method to the original delegate if there is one different than ourselves.
if (navigationController.delegate != self) {
    [navigationController.delegate navigationController:navigationController
                                 willShowViewController:viewController
                                               animated:animated];
}
}


  + (void)load {
//-- Exchange the original implementation with our custom one.
method_exchangeImplementations(class_getInstanceMethod(self,  @selector(pushViewController:animated:)), class_getInstanceMethod(self, @selector(safePushViewController:animated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(didShowViewController:animated:)), class_getInstanceMethod(self, @selector(safeDidShowViewController:animated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popViewControllerAnimated:)), class_getInstanceMethod(self, @selector(safePopViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popToRootViewControllerAnimated:)), class_getInstanceMethod(self, @selector(safePopToRootViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popToViewController:animated:)), class_getInstanceMethod(self, @selector(safePopToViewController:animated:)));
 }

 @end

Ошибка приложения iOS - Невозможно добавить себя как подвью

Ответ 1

Обновленный ответ:

Я предпочитаю это решение nonamelive в Github для того, что я изначально разместил: https://gist.github.com/nonamelive/9334458. Подклассифицируя UINavigationController и используя UINavigationControllerDelegate, вы можете установить, когда происходит переход, предотвратить другие переходы во время этого перехода и сделать все это в одном классе. Здесь обновление решения nonamelive, которое исключает частный API:

#import "NavController.h"

@interface NavController ()

@property (nonatomic, assign) BOOL shouldIgnorePushingViewControllers;

@end

@implementation NavController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (!self.shouldIgnorePushingViewControllers)
    {
        [super pushViewController:viewController animated:animated];
    }
    self.shouldIgnorePushingViewControllers = YES;
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    self.shouldIgnorePushingViewControllers = NO;
}

@end

Предыдущий ответ:

Проблема с этим предыдущим ответом: isBeingPresented и isBeingDismissed работают только в viewDidLoad: или viewDidApper:

Хотя я сам не тестировал это, вот предложение.

Поскольку вы используете UINavigationController, вы можете получить доступ к содержимому своего навигационного стека, например:

NSArray *viewControllers = self.navigationController.viewControllers;

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

К счастью, в iOS 5 были введены два особенно удобных метода: isBeingPresented и isBeingDismissed, которые возвращают "ДА", если контроллер представления находится в процессе представления или увольнения соответственно; "НЕТ" в противном случае.

Итак, например, здесь один подход:

NSArray *viewControllers = self.navigationController.viewControllers;

for (UIViewController *viewController in viewControllers) {

    if (viewController.isBeingPresented || viewController.isBeingDismissed) {
        // In this case when a pop or push is already in progress, don't perform
        // a pop or push on the current view controller. Perhaps return to this
        // method after a delay to check this conditional again.
        return;
    }
}

// Else if you make it through the loop uninterrupted, perform push or pop
// of the current view controller.

На самом деле вам, вероятно, не придется проходить через каждый контроллер представления в стеке, но, возможно, это предложение поможет вам отключиться на правой ноге.

Ответ 2

Вот мой подход, используя категорию UINavigationController и метод swizzling. Метод -[UINavigationController didShowViewController:animated:] является конфиденциальным, поэтому, хотя было сообщено о безопасности использования, используйте его собственные риски.

Кредиты идут на этот ответ для идеи и NSHipster для метод swizzling code. Этот ответ также имеет интересный подход.

//
//  UINavigationController+Additions.h
//

@interface UINavigationController (Additions)

@property (nonatomic, getter = isViewTransitionInProgress) BOOL viewTransitionInProgress;

@end


//
//  UINavigationController+Additions.m
//

#import "UINavigationController+Additions.h"
#import <objc/runtime.h>

static void *UINavigationControllerViewTransitionInProgressKey = &UINavigationControllerViewTransitionInProgressKey;

@interface UINavigationController ()
// Private method, use at your own risk.
- (void)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
@end


@implementation UINavigationController (Additions)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector1 = @selector(pushViewController:animated:);
        SEL swizzledSelector1 = @selector(zwizzledForViewTransitionInProgress_pushViewController:animated:);

        Method originalMethod1 = class_getInstanceMethod(class, originalSelector1);
        Method swizzledMethod1 = class_getInstanceMethod(class, swizzledSelector1);

        BOOL didAddMethod1 = class_addMethod(class, originalSelector1, method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1));

        if (didAddMethod1) {
            class_replaceMethod(class, swizzledSelector1, method_getImplementation(originalMethod1), method_getTypeEncoding(originalMethod1));
        } else {
            method_exchangeImplementations(originalMethod1, swizzledMethod1);
        }

        SEL originalSelector2 = @selector(didShowViewController:animated:);
        SEL swizzledSelector2 = @selector(zwizzledForViewTransitionInProgress_didShowViewController:animated:);

        Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
        Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);

        BOOL didAddMethod2 = class_addMethod(class, originalSelector2, method_getImplementation(swizzledMethod2), method_getTypeEncoding(swizzledMethod2));

        if (didAddMethod2) {
            class_replaceMethod(class, swizzledSelector2, method_getImplementation(originalMethod2), method_getTypeEncoding(originalMethod2));
        } else {
            method_exchangeImplementations(originalMethod2, swizzledMethod2);
        }

    });
}

- (void)zwizzledForViewTransitionInProgress_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (self.viewTransitionInProgress) {
        LogWarning(@"Pushing a view controller while an other view transition is in progress. Aborting.");
    } else {
        self.viewTransitionInProgress = YES;
        [self zwizzledForViewTransitionInProgress_pushViewController:viewController animated:animated];
    }
}

- (void)zwizzledForViewTransitionInProgress_didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [self zwizzledForViewTransitionInProgress_didShowViewController:viewController animated:YES];
    self.viewTransitionInProgress = NO;
}

- (void)setViewTransitionInProgress:(BOOL)viewTransitionInProgress
{
    NSNumber *boolValue = [NSNumber numberWithBool:viewTransitionInProgress];
    objc_setAssociatedObject(self, UINavigationControllerViewTransitionInProgressKey, boolValue, OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)isViewTransitionInProgress
{
    NSNumber *viewTransitionInProgress = objc_getAssociatedObject(self, UINavigationControllerViewTransitionInProgressKey);
    return [viewTransitionInProgress boolValue];
}

@end

Ответ 3

Вдохновленный @Lindsey Scott ответ, я создал подкласс UINavigationController. Преимущество моего решения в том, что он также обрабатывает всплывающие окна, и вы можете фактически выполнять все запросы друг за другом без проблем (это контролируется флажком acceptConflictingCommands).

MyNavigationController.h

#import <UIKit/UIKit.h>

@interface MyNavigationController : UINavigationController

@property(nonatomic, assign) BOOL acceptConflictingCommands;

@end

MyNavigationController.m

#import "MyNavigationController.h"

@interface MyNavigationController ()<UINavigationControllerDelegate>

@property(nonatomic, assign) BOOL shouldIgnoreStackRequests;
@property(nonatomic, strong) NSMutableArray* waitingCommands;

@end

@implementation MyNavigationController

-(instancetype)init
{
    if( self = [super init] )
    {
        self.delegate = self;
        _waitingCommands = [NSMutableArray new];
    }

    return self;
}

-(instancetype)initWithRootViewController:(UIViewController *)rootViewController
{
    if( self = [super initWithRootViewController:rootViewController] )
    {
        self.delegate = self;
        _waitingCommands = [NSMutableArray new];
        _acceptConflictingCommands = YES;
    }

    return self;
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if( !_shouldIgnoreStackRequests )
    {
        [super pushViewController:viewController animated:animated];

        _shouldIgnoreStackRequests = YES;
    }
    else if (_acceptConflictingCommands)
    {
        __weak typeof(self) weakSelf = self;

        //store and push it after current transition ends
        [_waitingCommands addObject:^{

            id strongSelf = weakSelf;

            [strongSelf pushViewController:viewController animated:animated];

        }];
    }

}

-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    __block UIViewController* popedController = nil;

    if( 1 < self.viewControllers.count )
    {
        if( !_shouldIgnoreStackRequests )
        {
            popedController = [super popViewControllerAnimated:animated];

            _shouldIgnoreStackRequests = YES;
        }
        else if( _acceptConflictingCommands )
        {
            __weak typeof(self) weakSelf = self;

            [_waitingCommands addObject:^{

                id strongSelf = weakSelf;

                popedController = [strongSelf popViewControllerAnimated:animated];

            }];
        }
    }

    return popedController;
}

#pragma mark - uinavigationcontroller delegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    _shouldIgnoreStackRequests = NO;

    if( 0 < _waitingCommands.count )
    {
        void(^waitingAction)() = _waitingCommands.lastObject;
        [_waitingCommands removeLastObject];
        waitingAction();
    }
}

@end

Конечно, вы можете изменить значение по умолчанию acceptConflictingCommands или контролировать его извне.

Если ваш код использует popToRootViewController, setViewControllers: анимированный: и/или popToViewController, вы должны переопределить их таким же образом, чтобы убедиться, что они не будут блокировать стек навигации.