web-dev-qa-db-fra.com

erreur d'application iOS - Impossible d'ajouter l'auto comme sous-vue

J'ai reçu ce rapport d'accident, mais je ne sais pas comment le déboguer.

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

La version iOS est 7.0.3 . Quelqu'un at-il vécu ce crash étrange?

METTRE À JOUR:

Je ne sais pas où dans mon code a provoqué cet accident, je ne peux donc pas poster le code ici, désolé.

Deuxième MISE À JOUR

Voir la réponse ci-dessous.

149
Arnol

Je vais décrire plus en détail cet incident dans mon application et le marquer comme étant résolu.

Mon application a un UINavigationController avec le contrôleur racine est un UITableViewController qui contient une liste d'objets de note. L'objet note a une propriété de contenu au format HTML. Sélectionnez une note qui ira au contrôleur de détail.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    //get note object
    DetailViewController *controller = [[DetailViewController alloc] initWithNote:note];
    [self.navigationController pushViewController:controller animated:YES];
}

Contrôleur de détail

Ce contrôleur a un UIWebView, affiche le contenu de la note transmise par le contrôleur racine.

- (void)viewDidLoad
{
    ...
    [_webView loadHTMLString:note.content baseURL:nil];
    ...
}

Ce contrôleur est le délégué du contrôle WebView. Si la note contient des liens, appuyez sur un lien pour accéder au navigateur Web intégré à l'application.

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

J'ai reçu le rapport d'accident ci-dessus tous les jours. Je ne sais pas où dans mon code a provoqué ce crash. Après quelques recherches avec l’aide d’un utilisateur, j’ai finalement pu réparer ce crash. Ce contenu HTML provoquera le crash:

...
<iframe src="http://google.com"></iframe>
...

Dans la méthode viewDidLoad du contrôleur de détail, j'ai chargé ce code HTML dans le contrôle webview. Juste après, la méthode de délégation ci-dessus a été appelée immédiatement avec request.URL est la source de l'iframe (google.com). Cette méthode de délégué appelle la méthode pushViewController dans viewDidLoad => crash!

J'ai corrigé ce crash en vérifiant le type de navigation:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if (navigationType != UIWebViewNavigationTypeOther)
    {
        //go to web browser controller
    }
}

J'espère que cela t'aides

8
Arnol

Je spécule sur quelque chose de similaire que j'ai débogué récemment ... Si vous poussez (ou sautez) un contrôleur de vue avec Animated: OUI, cela ne se termine pas tout de suite, et de mauvaises choses arrivent si vous faites un autre Push ou pop avant la fin de l'animation. Vous pouvez facilement vérifier si c'est effectivement le cas en modifiant temporairement vos opérations Push et Pop en Animated: NO (afin qu'elles se terminent de manière synchrone) et en vérifiant si cela élimine le blocage. Si c'est bien votre problème et si vous souhaitez pour réactiver l'animation, la stratégie appropriée consiste à implémenter le protocole UINavigationControllerDelegate . Cela inclut la méthode suivante, appelée une fois l'animation terminée:

navigationController:didShowViewController:animated:

En gros, vous voulez déplacer le code si nécessaire dans cette méthode pour vous assurer qu'aucune autre action susceptible de modifier la pile NavigationController ne se produira jusqu'à ce que l'animation soit terminée et que la pile soit prête pour d'autres modifications.

48
RobP

Nous avons également commencé à avoir ce problème et il était fort probable que le même problème soit à l'origine du nôtre.

Dans notre cas, nous avons parfois dû extraire des données de l’arrière-guichet, ce qui signifiait qu’un utilisateur pouvait exploiter quelque chose puis qu’un léger délai s’écoulait avant l’apparition du nav Push. Si un utilisateur cherchait rapidement, il pourrait se retrouver avec deux poussées de navigation provenant du même contrôleur de vue, ce qui a déclenché cette exception.

Notre solution est une catégorie du UINavigationController qui empêche les push/pops à moins que le sommet vc soit le même à partir d’un moment donné.

fichier .h:

@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

fichier .m:

@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

Jusqu'à présent, cela semble avoir résolu le problème pour nous. Exemple:

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

Fondamentalement, la règle est la suivante: avant tout délais non liés à l'utilisateur saisissez un verrou du contrôleur de navigation approprié et incluez-le dans l'appel à Push/pop.

Le mot "verrouiller" est peut-être un libellé un peu médiocre, car il pourrait laisser supposer qu'il doit être déverrouillé sous une forme ou une autre, mais comme il n'y a pas de méthode de "déverrouillage" nulle part, ce n'est probablement pas grave.

(En résumé, les "retards non liés à l'utilisateur" sont tous les retards causés par le code, c'est-à-dire tout ce qui est asynchrone. Les utilisateurs qui appuient sur un contrôleur de navigation qui est poussé de manière animée ne comptent pas et il n'est pas nécessaire de faire navigationLock: version pour ceux cas.)

13
Kalle

Ce code résout le problème: https://Gist.github.com/nonamelive/9334458

Il utilise une API privée, mais je peux confirmer qu'il est sécurisé sur l'App Store. (Une de mes applications utilisant ce code a été approuvée par l'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;
}
12
nonamelive

J'ai eu le même problème, ce qui a simplement fonctionné pour moi était en train de changer.

Il semble que le problème soit dû au fait que l'animation n'a pas été terminée à temps.

J'espère que ça aide quelqu'un.

5
Lion789

Pour reproduire ce bogue, essayez d’appuyer deux contrôleurs de vue en même temps. Ou pousser et apparaître en même temps. Exemple:

enter image description here J'ai créé une catégorie qui intercepte ces appels et les sécurise en veillant à ce qu'aucune autre impulsion ne se produise en même temps. Il vous suffit de copier le code dans votre projet et, grâce à la méthode, vous serez prêt à partir.

#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
3
dan

Désolé d'être en retard pour la fête. J'ai récemment eu ce problème dans lequel ma barre de navigation passe à l'état corrompu en raison de la poussée de plus d'un contrôleur de vue en même temps. Cela est dû au fait que l'autre contrôleur de vue est enfoncé pendant que le premier contrôle d'animation est encore en cours d'animation. Prenant allusion à la réponse non-amélique, j’ai proposé une solution simple qui fonctionne dans mon cas. Il vous suffit de sous-classer UINavigationController, de remplacer la méthode pushViewController et de vérifier si l'animation du contrôleur de vue précédente est terminée. Vous pouvez écouter l’achèvement de l’animation en faisant de votre classe un délégué de UINavigationControllerDelegate et en définissant le délégué sur self.

J'ai téléchargé un Gist ici pour simplifier les choses.

Assurez-vous simplement de définir cette nouvelle classe en tant que NavigationController dans votre storyboard.

1
nikhil.thakkar

Je pense que pousser/faire apparaître des contrôleurs de vue avec animation à tout moment devrait être parfaitement correct et que le SDK devrait gérer gracieusement la file d’appels pour nous.

Par conséquent, ce n'est pas le cas et toutes les solutions tentent d'ignorer les poussées suivantes, ce qui pourrait être considéré comme un bogue, car la pile de navigation finale ne correspond pas à ce que le code prévoyait.

J'ai mis en place une file d'attente d'appels Push à la place:

// SafeNavigationController.h

@interface SafeNavigationController : UINavigationController
@end

// SafeNavigationController.m

#define timeToWaitBetweenAnimations 0.5

@interface SafeNavigationController ()

@property (nonatomic, strong) NSMutableArray * controllersQueue;
@property (nonatomic)         BOOL animateLastQueuedController;
@property (nonatomic)         BOOL pushScheduled;
@property (nonatomic, strong) NSDate * lastAnimatedPushDate;

@end

@implementation SafeNavigationController

- (void)awakeFromNib
{
    [super awakeFromNib];

    self.controllersQueue = [NSMutableArray array];
}

- (void)pushViewController:(UIViewController *)viewController
                  animated:(BOOL)animated
{
    [self.controllersQueue addObject:viewController];
    self.animateLastQueuedController = animated;

    if (self.pushScheduled)
        return;

    // Wait for Push animation to finish
    NSTimeInterval timeToWait = self.lastAnimatedPushDate ? timeToWaitBetweenAnimations + [self.lastAnimatedPushDate timeIntervalSinceNow] : 0.0;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((timeToWait > 0.0 ? timeToWait : 0.0) * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^
                   {
                       [self pushQueuedControllers];

                       self.lastAnimatedPushDate = self.animateLastQueuedController ? [NSDate date] : nil;
                       self.pushScheduled = NO;
                   });
    self.pushScheduled = YES;
}

- (void)pushQueuedControllers
{
    for (NSInteger index = 0; index < (NSInteger)self.controllersQueue.count - 1; index++)
    {
        [super pushViewController:self.controllersQueue[index]
                         animated:NO];
    }
    [super pushViewController:self.controllersQueue.lastObject
                     animated:self.animateLastQueuedController];

    [self.controllersQueue removeAllObjects];
}

@end

Il ne gère pas les files d'attente mixtes Push et Pops, mais c'est un bon débutant pour réparer la plupart de nos accidents.

Gist: https://Gist.github.com/rivera-ernesto/0bc628be1e24ff5704ae

1
Rivera

Je viens de vivre ce problème aussi. Laissez moi vous montrer mon code: 

override func viewDidLoad() { 
  super.viewDidLoad()

  //First, I create a UIView
  let firstFrame = CGRect(x: 50, y: 70, height: 200, width: 200)
  let firstView = UIView(frame: firstFrame)
  firstView.addBackgroundColor = UIColor.yellow
  view.addSubview(firstView) 

  //Now, I want to add a subview inside firstView
  let secondFrame = CGRect(x: 20, y:50, height: 15, width: 35)
  let secondView = UIView(frame: secondFrame)
  secondView.addBackgroundColor = UIColor.green
  firstView.addSubView(firstView)
 }

L'erreur est due à cette ligne: 

firstView.addSubView(firstView)

Vous ne pouvez pas vous ajouter à la sous-vue. J'ai changé la ligne de code en: 

firstView.addSubView(secondView)

L'erreur est partie et j'ai pu voir les deux points de vue. Je pensais que cela aiderait tous ceux qui veulent voir un exemple.

1
halapgos1

Recherchez dans votre code "addSubview".

Dans l'un des emplacements où vous avez appelé cette méthode, vous avez essayé d'ajouter une vue à son propre tableau de sous-vues à l'aide de cette méthode.

Par exemple:

[self.view addSubview:self.view];

Ou:

[self.myLabel addSubview:self.myLabel];
1
Michal Shatz

la solution de nonamelive est géniale. Mais si vous ne voulez pas utiliser l’API privée, vous pouvez simplement utiliser la méthode UINavigationControllerDelegate.Ou vous pouvez modifier la YES animée en NO. Voici un exemple de code, vous pouvez en hériter . Hope C'est utile : )

https://github.com/antrix1989/ANNavigationController

0
NSKevin

Sur la base de @RobP, j'ai fait une bonne/ sous-classe UINavigationController afin d'éviter de tels problèmes. Il gère les poussées et/ou les éclats et vous pouvez exécuter en toute sécurité:

[self.navigationController pushViewController:vc1 animated:YES];
[self.navigationController pushViewController:vc2 animated:YES];
[self.navigationController pushViewController:vc3 animated:YES];
[self.navigationController popViewControllerAnimated:YES];

Si 'acceptConflictingCommands' le marque true (par défaut), l'utilisateur verra l'animation poussée de vc1, vc2, vc3, puis verra son éclatement animé. Si 'acceptConflictingCommands' est false, toutes les demandes Push/pop seront rejetées jusqu'à ce que vc1 soit entièrement transmise - les 3 autres appels seront donc rejetés.

0
hris.to

Parfois, vous avez essayé par erreur d’ajouter une vue à sa propre vue.

halfView.addSubview(halfView)

changez ceci en votre vue secondaire.

halfView.addSubview(favView)
0
Vinoth Vino

J'avais beaucoup cherché ce problème, cela poussait peut-être deux ou plus VC en même temps, ce qui causait le problème d'animation poussant, .__, vous pouvez vous référer à ceci: Impossible d'ajouter l'auto comme sous-vue 解决办法

assurez-vous simplement qu'il y a un VC sur la transition en même temps - bonne chance.

0
FFur