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, вы должны переопределить их таким же образом, чтобы убедиться, что они не будут блокировать стек навигации.