iOS app error - Can't add self as subview
I received this crash report, but I don't know how to debug it.
Fatal Exception NSInvalidArgumentException
Can't add self as subview
0 ... CoreFoundation __exceptionPreprocess + 130
1 libobjc.A.dylib objc_exception_throw + 38
2 CoreFoundation -[NSException initWithCoder:]
3 UIKit -[UIView(Internal) _addSubview:positioned:relativeTo:] + 110
4 UIKit -[UIView(Hierarchy) addSubview:] + 30
5 UIKit __53-[_UINavigationParallaxTransition animateTransition:]_block_invoke + 1196
6 UIKit +[UIView(Animation) performWithoutAnimation:] + 72
7 UIKit -[_UINavigationParallaxTransition animateTransition:] + 732
8 UIKit -[UINavigationController _startCustomTransition:] + 2616
9 UIKit -[UINavigationController _startDeferredTransitionIfNeeded:] + 418
10 UIKit -[UINavigationController __viewWillLayoutSubviews] + 44
11 UIKit -[UILayoutContainerView layoutSubviews] + 184
12 UIKit -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 346
13 QuartzCore -[CALayer layoutSublayers] + 142
14 QuartzCore CA::Layer::layout_if_needed(CA::Transaction*) + 350
15 QuartzCore CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 16
16 QuartzCore CA::Context::commit_transaction(CA::Transaction*) + 228
17 QuartzCore CA::Transaction::commit() + 314
18 QuartzCore CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 56
The iOS version is 7.0.3. Anyone experience this weird crash?
UPDATE:
I don't know where in my code caused this crash, so I can not post the code here, sorry.
Second UPDATE
See the answer below.
Solution 1:
I am speculating based on something similar that I debugged recently... if you push (or pop) a view controller with Animated:YES it doesn't complete right away, and bad things happen if you do another push or pop before the animation completes. You can easily test whether this is indeed the case by temporarily changing your Push and Pop operations to Animated:NO (so that they complete synchronously) and seeing if that eliminates the crash. If this is indeed your problem and you wish to turn animation back ON, then the correct strategy is to implement the UINavigationControllerDelegate protocol. This includes the following method, which is called after the animation is complete:
navigationController:didShowViewController:animated:
Basically you want to move some code as needed into this method to ensure that no other actions that could cause a change to the NavigationController stack will occur until the animation is finished and the stack is ready for more changes.
Solution 2:
We started getting this issue as well, and chances were highly likely that ours were caused by the same problem.
In our case, we had to pull data from the back end in some cases, which meant a user might tap something and then there'd be a slight delay before the nav push occurred. If a user was rapidly tapping around, they might end up with two nav pushes from the same view controller, which triggered this very exception.
Our solution is a category on the UINavigationController which prevents pushes/pops unless the top vc is the same one from a given point in time.
.h file:
@interface UINavigationController (SafePushing)
- (id)navigationLock; ///< Obtain "lock" for pushing onto the navigation controller
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock; ///< Uses a horizontal slide transition. Has no effect if the view controller is already in the stack. Has no effect if navigationLock is not the current lock.
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock; ///< Pops view controllers until the one specified is on top. Returns the popped controllers. Has no effect if navigationLock is not the current lock.
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated navigationLock:(id)navigationLock; ///< Pops until there's only a single view controller left on the stack. Returns the popped controllers. Has no effect if navigationLock is not the current lock.
@end
.m file:
@implementation UINavigationController (SafePushing)
- (id)navigationLock
{
return self.topViewController;
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
[self pushViewController:viewController animated:animated];
}
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
return [self popToRootViewControllerAnimated:animated];
return @[];
}
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
return [self popToViewController:viewController animated:animated];
return @[];
}
@end
So far this seems to have resolved the problem for us. Example:
id lock = _dataViewController.navigationController.navigationLock;
[[MyApi sharedClient] getUserProfile:_user.id success:^(MyUser *user) {
ProfileViewController *pvc = [[ProfileViewController alloc] initWithUser:user];
[_dataViewController.navigationController pushViewController:pvc animated:YES navigationLock:lock];
}];
Basically, the rule is: before any non user related delays grab a lock from the relevant nav controller, and include it in the call to push/pop.
The word "lock" may be slightly poor wording as it may insinuate there's some form of lock happening that needs unlocking, but since there's no "unlock" method anywhere, it's probably okay.
(As a sidenote, "non user related delays" are any delays that the code is causing, i.e. anything asynchronous. Users tapping on a nav controller which is animatedly pushed doesn't count and there's no need to do the navigationLock: version for those cases.)
Solution 3:
This code resolves the issue: https://gist.github.com/nonamelive/9334458
It uses a private API, but I can confirm that it's App Store safe. (One of my apps using this code got approved by the App Store.)
@interface UINavigationController (DMNavigationController)
- (void)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
@end
@interface DMNavigationController ()
@property (nonatomic, assign) BOOL shouldIgnorePushingViewControllers;
@end
@implementation DMNavigationViewController
#pragma mark - Push
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (!self.shouldIgnorePushingViewControllers)
{
[super pushViewController:viewController animated:animated];
}
self.shouldIgnorePushingViewControllers = YES;
}
#pragma mark - Private API
// 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)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[super didShowViewController:viewController animated:animated];
self.shouldIgnorePushingViewControllers = NO;
}
Solution 4:
I will describe more details about this crash in my app and mark this as answered.
My app has a UINavigationController with the root controller is a UITableViewController that contains a list of note objects. The note object has a content property in html. Select a note will go to the detail controller.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//get note object
DetailViewController *controller = [[DetailViewController alloc] initWithNote:note];
[self.navigationController pushViewController:controller animated:YES];
}
Detail controller
This controller has a UIWebView, display the note content passed from the root controller.
- (void)viewDidLoad
{
...
[_webView loadHTMLString:note.content baseURL:nil];
...
}
This controller is the delegate of the webview control. If the note contains links, tap a link will go to the in-app web browser.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
WebBrowserViewController *browserController = [[WebBrowserViewController alloc] init];
browserController.startupURL = request.URL;
[self.navigationController pushViewController:webViewController animated:YES];
return NO;
}
I received the above crash report everyday. I don't know where in my code caused this crash. After some investigates with the help of a user, I was finally able to fix this crash. This html content will cause the crash:
...
<iframe src="http://google.com"></iframe>
...
In the viewDidLoad method of the detail controller, I loaded this html to the webview control, right after that, the above delegate method was called immediately with request.URL is the iframe's source (google.com). This delegate method calls pushViewController method while in viewDidLoad => crash!
I fixed this crash by checking the navigationType:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (navigationType != UIWebViewNavigationTypeOther)
{
//go to web browser controller
}
}
Hope this helps
Solution 5:
I had the same issue, what simply worked for me was changing Animated:Yes to Animated:No.
It looks like the issue was due to the animation not completing in time.
Hope this helps someone.