📜 ⬆️ ⬇️

How to scratch any application on the iPhone, and how to prevent it

image

Once we, in Surfingbird , found a strange error that caused the application to crash steadily. Later it turned out that almost any application can be pretty simple to scratch (even applications written by Apple itself). That this for an error and how to bypass it, we will tell in article.

At once we will specify, everything described is true for iOS 7 and less. About what has changed in iOS 8 - at the end of the article (nothing good, in fact).

Let's start with practice. There are 2 buttons, each of them shows a new screen. Just press both buttons simultaneously (you need to practice a little) and then 2 times back:

image
')
In order to drop the app, we need a navigationController. If you launch the viewController (with animation) in the navigationController, then, without waiting for the animation to complete, push the second viewController and press the back button 2 times, then the application will crash. At first it sounds like nonsense, because no one would do that. However, you should not forget that there is a multitouch in the iPhone and at the same time you can press several buttons. Actually, the code is not complicated at all;

@interface ViewController () @property (strong, nonatomic) UIButton *buttonL; @property (strong, nonatomic) UIButton *buttonR; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.title = @"root"; self.view.backgroundColor = [UIColor whiteColor]; self.buttonL = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)]; self.buttonL.backgroundColor = [UIColor blueColor]; [self.buttonL setTitle:@"push vc #1" forState:UIControlStateNormal]; [self.buttonL addTarget:self action:@selector(pushViewControllerOne) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.buttonL]; self.buttonR = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)]; self.buttonR.backgroundColor = [UIColor redColor]; [self.buttonR setTitle:@"push vc #2" forState:UIControlStateNormal]; [self.buttonR addTarget:self action:@selector(pushViewControllerTwo) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.buttonR]; } - (void) viewWillLayoutSubviews { CGFloat width = self.view.bounds.size.width /2; CGFloat height = self.view.bounds.size.height; [self.buttonL setFrame:CGRectMake(0.0f, 0.0f, width, height)]; [self.buttonR setFrame:CGRectMake(width, 0.0f, width, height)]; } - (void) pushViewControllerOne { UIViewController *vc1 = [UIViewController new]; vc1.navigationItem.title = @"#1"; vc1.view.backgroundColor = [UIColor whiteColor]; [self.navigationController pushViewController:vc1 animated:YES]; } - (void) pushViewControllerTwo { UIViewController *vc1 = [UIViewController new]; vc1.navigationItem.title = @"#2"; vc1.view.backgroundColor = [UIColor whiteColor]; [self.navigationController pushViewController:vc1 animated:YES]; } @end 

If you look in the Xcode logs, you can see the warnings about the embedded animation and possible damage to the navigation bar:
nested push animation can result in corrupted navigation bar
Finishing up a navigation transition. Navigation Bar subview tree might get corrupted.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Can't add self as subview'

Online description of this error is very rare , and the solution was found only one, and it does not work. Therefore, we decided to become Santa Clauses and give the community a solution to a problem that Apple cannot solve .

The solution to the problem is very obvious: we inherit from UINavigationController, put all the pushes into a queue, then execute them in turn. Some of the code needed to understand the implementation is described below:

 // // StackNavigationController.m // #import "StackNavigationController.h" @interface StackNavigationController () <UINavigationControllerDelegate> @property (nonatomic, assign) BOOL isTransitioning; @property (nonatomic, strong) NSMutableArray *tasks; @property (nonatomic, weak) id<UINavigationControllerDelegate> customDelegate; @end @implementation StackNavigationController -(void)viewDidLoad { [super viewDidLoad]; if (self.delegate) { self.customDelegate = self.delegate; } self.delegate = self; self.tasks = [NSMutableArray new]; } // we should save navController.delegate to another property because we need delegate // to prevent multiple push/pop bug -(void)setDelegate:(id<UINavigationControllerDelegate>)delegate { if (delegate == self) { [super setDelegate:delegate]; } else { self.customDelegate = delegate; } } - (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated { @synchronized(self.tasks) { if (self.isTransitioning) { void (^task)(void) = ^{ [self pushViewController:viewController animated:animated]; }; [self.tasks addObject:task]; } else { self.isTransitioning = YES; [super pushViewController:viewController animated:animated]; } } } - (void) runNextTask { @synchronized(self.tasks) { if (self.tasks.count) { void (^task)(void) = self.tasks[0]; [self.tasks removeObjectAtIndex:0]; task(); } } } #pragma mark UINavigationControllerDelegate -(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { self.isTransitioning = NO; if ([self.customDelegate respondsToSelector:@selector(navigationController:didShowViewController:animated:)]) { [self.customDelegate navigationController:navigationController didShowViewController:viewController animated:animated]; } // black magic :) // if one of push/pop will be without animation - we should place this code to the end of runLoop to prevent bad behavior [self performSelector:@selector(runNextTask) withObject:nil afterDelay:0.0f]; } @end 

All code can be found on the githaba .

In recent versions of iOS, the situation has improved slightly. If earlier in iOS 7 and less, the application crashed while simultaneously pressing two buttons, then now in iOS 8, this will require 3 buttons . But crash is inevitable anyway.

Again, applying this practice you can cross almost any application. For example, we consistently manage to crash even the App Store . It is not clear why Apple does not consider this a problem and does not deal with its solution. Have you encountered a similar problem in your projects, and how was it solved?

PS in the comments ASkvortsov offers to use the exclusiveTouch property

Source: https://habr.com/ru/post/246757/


All Articles