Bob Spryn
Bob Spryn

Reputation: 17812

Illegal constrain error when flipping back to tableView after reloadData

This one is really freaking weird. Here's the error:

***** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to install constraint on view. Does the constraint reference something from outside the subtree of the view? That's illegal. constraint: view:>'**

So my custom cell has a subview, that I is added in the configuration step. That is, I deuque a cell, then configure it with a data object. In the configuration step, it adds the third party subview if it doesn't exist:

if (!self.thirdPartyAnswerView) {
    self.thirdPartyAnswerView = [TCThirdPartyAPIHelper thirdPartyAnswerViewForThirdPartyAPIServiceType:answer.thirdPartyObject.thirdPartyAPIType];
    self.thirdPartyAnswerView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.contentWrapperView addSubview:self.thirdPartyAnswerView];
    NSDictionary *metrics = @{
                              @"TC_CELL_TOP_PADDING": [NSNumber numberWithFloat:TC_CELL_TOP_PADDING],
                              @"TC_CELL_BOTTOM_PADDING": [NSNumber numberWithFloat:TC_CELL_BOTTOM_PADDING],
                              @"TC_CELL_RIGHT_PADDING": [NSNumber numberWithFloat:TC_CELL_RIGHT_PADDING],
                              @"TC_CELL_LEFT_PADDING": [NSNumber numberWithFloat:TC_CELL_LEFT_PADDING],
                              };
    [self.contentWrapperView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-TC_CELL_LEFT_PADDING-[_thirdPartyAnswerView]-TC_CELL_RIGHT_PADDING-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(_thirdPartyAnswerView)]];
    [self.contentWrapperView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-TC_CELL_TOP_PADDING-[_thirdPartyAnswerView]-TC_CELL_BOTTOM_PADDING-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(_thirdPartyAnswerView)]];
}

This all works fine. In fact, it works great. I step into my answer cells (which presents a modal edit/creation flow), and then finish and come back to the same screen. I update the rows nicely with animations instead of calling reloadData.

Screen

The problem comes in when I hit the category button after I have existing data. It pulls up a category selection modal. When I choose a category, it calls reloadData in the previous screen, and then animates the category modal away. Well, at least that's what it's supposed to do. Instead it crashes immediately at the start of the animation. It calls reloadData without a problem, but as soon as the animation starts, boom. It either throws the crash I showed above, doesn't provide any explanation, or crashes with the complaint of _supportsContentDimensionVariables selector being sent to the wrong object.

Since the view used can vary between cells, in the call prepareForReuse I remove the thirdPartyAnswerView. If I remove this, it doesn't crash (but then I have a view in my cell I don't want). I tried removing the constraints before and after:

if (self.thirdPartyAnswerView) {
    [self.contentWrapperView removeConstraints:self.thirdPartyAnswerView.constraints];
    [self.thirdPartyAnswerView removeFromSuperview];
    self.thirdPartyAnswerView = nil;
}

That didn't work. I tried moving this stuff out of prepareForReuse and into the configuration section (remove the subview before adding a new one). That didn't work. I'm at a loss.

Update

Here is the code called when a category in the modal is chosen:

categorySelectionVC.didTouchCategoryBlock = ^(TCCategory *category) {
    TCQuestionBuilderViewController *sself = weakself;
    sself.category = category;
    [sself.tableView reloadData];
    [sself.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:TableSectionCategory] animated:NO scrollPosition:UITableViewScrollPositionNone];
    [sself dismissViewControllerAnimated:YES completion:^{
    }];
};

What I've since determined, is that if I comment out the selectRowAtIndexPath bit after the reload, it works properly. What I don't understand is why. I've used this many times. I reloadData, reselect what was selected so that it animates "off" when the tableView is seen again.

I was incorrect. While removing that did solve it initially, if you entered the modal again and then selected a category again it crashed.

SOLVED

Thanks to sapi, I figured out that constraints weren't being removed properly. Using the constraints property off of thirdPartyView doesn't include the constraints stored on a parent view. Had to use this syntax, although I'm guessing there's a shorter way of doing this:

for (NSLayoutConstraint *constraint in self.contentWrapperView.constraints) {
    if (constraint.firstItem == self.thirdPartyAnswerView || constraint.secondItem == self.thirdPartyAnswerView) {
        [constraints addObject:constraint];
    }
}
[self.contentWrapperView removeConstraints:constraints];

Upvotes: 2

Views: 906

Answers (1)

sapi
sapi

Reputation: 10224

Constraints are bi-directional, but the reverse relationship is represented by a different object to the forward relationship.

If you create a constraint using, for example:

[self.contentWrapperView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-TC_CELL_LEFT_PADDING-[_thirdPartyAnswerView]-TC_CELL_RIGHT_PADDING-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(_thirdPartyAnswerView)]];

then self.contentWrapperView and _thirdPartyAnswerView both have constraints added to them, but while they both represent the same relationship, they are different objects.

When you call:

[self.contentWrapperView removeConstraints:self.thirdPartyAnswerView.constraints];

this attempts to remove all constraints on self.contentWrapperView which are identical to those on self.thirdPartyAnswerView, ie none.

To remove the appropriate constraints, you have to loop through the array and identify those referring to the thirdPartyAnswerView (see edit to question for one way of doing this).

Upvotes: 2

Related Questions