40

I am configuring a custom UITableViewCell using a prototype cell in a Storyboard. However, all the UILabels (and other UI elements) do not seem to be added to the cell's contentView, instead being added to the UITableViewCell view directly. This creates issues when the cell is put into editing mode, as the content is not automatically shifted/indented (which it would do, if they were inside the contentView).

Is there any way to add the UI elements to the contentView when laying out the cell using Interface Builder/Storyboard/prototype cells? The only way I have found is to create everything in code and use [cell.contentView addSubView:labelOne] which wouldn't be great, as it is much easier to layout the cell graphically.

Skoota
  • 5,280
  • 9
  • 52
  • 75
  • 1
    Are you sure about that? Last time I laid out a cell in a nib, while it didn't look like I was adding sub views to the content view, at run time in the debugger everything was in the content view. Worth verifying in the debugger if you haven't already... – Carl Veazey Sep 26 '12 at 23:46
  • 1
    Thanks Carl. You were right - all subviews are added to the contentView. The problem was related to iOS 6 autolayout. I have included an answer which outlines how the problem was fixed. – Skoota Sep 30 '12 at 01:17

6 Answers6

67

On further investigation (viewing the subview hierarchy of the cell) Interface Builder does place subviews within the cell's contentView, it just doesn't look like it.

The root cause of the issue was iOS 6 autolayout. When the cell is placed into editing mode (and indented) the contentView is also indented, so it stands to reason that all subviews within the contentView will move (indent) by virtue of being within the contentView. However, all the autolayout constraints applied by Interface Builder seem to be relative to the UITableViewCell itself, rather than the contentView. This means that even though the contentView indents, the subviews contained within do not - the constraints take charge.

For example, when I placed a UILabel into the cell (and positioned it 10 points from the left-hand side of the cell) IB automatically applied a constraint "Horizontal Space (10)". However, this constraint is relative to the UITableViewCell NOT the contentView. This means that when the cell is indented, and the contentView moves, the label stays put as it is complying with the constraint to remain 10 points from the left-hand side of the UITableViewCell.

Unfortunately (as far as I am aware) there is no way to remove these IB created constraints from within IB itself, so here is how I solved the problem.

Within the UITableViewCell subclass for the cell, I created an IBOutlet for that constraint called cellLabelHSpaceConstraint. You also need an IBOutlet for the label itself, which I called cellLabel. I then implemented the -awakeFromNib method as per below:

- (void)awakeFromNib {

    // -------------------------------------------------------------------
    // We need to create our own constraint which is effective against the
    // contentView, so the UI elements indent when the cell is put into
    // editing mode
    // -------------------------------------------------------------------

    // Remove the IB added horizontal constraint, as that's effective
    // against the cell not the contentView
    [self removeConstraint:self.cellLabelHSpaceConstraint];

    // Create a dictionary to represent the view being positioned
    NSDictionary *labelViewDictionary = NSDictionaryOfVariableBindings(_cellLabel);   

    // Create the new constraint
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-10-[_cellLabel]" options:0 metrics:nil views:labelViewDictionary];

    // Add the constraint against the contentView
    [self.contentView addConstraints:constraints];

}

In summary, the above will remove the horizontal spacing constraint which IB automatically added (as is effective against the UITableViewCell rather than the contentView) and we then define and add our own constraint to the contentView.

In my case, all the other UILabels in the cell were positioned based upon the position of the cellLabel so when I fixed up the constraint/positioning of this element all the others followed suit and positioned correctly. However, if you have a more complex layout then you may need to do this for other subviews as well.

Skoota
  • 5,280
  • 9
  • 52
  • 75
  • Thanks for posting your detailed workaround. Had the same issue! – Prine Oct 08 '12 at 09:06
  • 3
    You can work around this by turning off autolayout in interface builder, either for the whole storyboard or just the nib containing your cell. See answer here for details: http://stackoverflow.com/questions/12833176/indentation-not-working-on-custom-uitableviewcell – jrturton Oct 11 '12 at 20:48
  • 1
    Yep, that's right. However, autolayout is extremely useful for orientation changes (particularly given the complex cells I have designed). In my use case, the overhead of this workaround while keeping autolayout enabled (versus having to completely relayout the cell manually when the device is rotated) is sufficient to justify it. However, hopefully this will be fixed in a future release... – Skoota Oct 25 '12 at 09:52
  • I've looked in to the code generated by IB and your right, all the constraints expressed in terms of TebleViewCell instead of the contentView (even if the contentView is present in the XML but not accessible from IB UI). It is possible to remove one outlet from your solution (cellLabelHSpaceConstraint) by expressing all the constraints in IB relative to the RIGHT side of the TableViewCell and giving to one with constraint a priority less than 1000. Using this method you only need one outlet and the original constraint doesn't need to be removed. – Adrian Nov 30 '12 at 09:00
  • 1
    +1 Thanks for posting this - I just used it and it works great! This does seem like a bug in the way IB handles constraints on `UITableViewCell` custom content. – Cubs Fan Ron Jan 04 '13 at 15:47
  • This definitely seems like a workaround for a bug. When the view class is a subclass of UITableViewCell, and things are being added to the contentView, it seems pretty clear that the constraints should be on the contentView. – Stefan Kendall Apr 20 '13 at 22:55
  • Don't forget to call [super awakeFromNib]; – Collin Jul 29 '13 at 17:36
  • Sorry to resurrect an old thread, but I'm having an issue. I'm using storyboard so I'm trying to add this to initWithCoder. However my label outlet has not yet been loaded into memory in initWithCoder. Any suggestions how I can use this solution with a storyboard file? Thanks!! – fogwolf Aug 23 '13 at 18:35
  • I should add that I'm using a static cell, so I am not implementing cellForRowAtIndexPath – fogwolf Aug 23 '13 at 18:45
33

As mentioned, XCode's Interface Builder is hiding the UITableViewCell's contentView. In reality, all UI elements added to the UITableViewCell are in fact subviews of the contentView.

For the moment, it IB is not doing the same magic for layout constraints, meaning that they are all expressed at UITableViewCell level.

A workaround is in a subclass's awakeFromNib to move all NSAutoLayoutConstrains from UITableViewCell to it's contentView and express them in terms of the contentView :

-(void)awakeFromNib{
  [super awakeFromNib];
  for(NSLayoutConstraint *cellConstraint in self.constraints){
    [self removeConstraint:cellConstraint];
    id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
    id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
    NSLayoutConstraint* contentViewConstraint =
    [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
    [self.contentView addConstraint:contentViewConstraint];
  }
}
Community
  • 1
  • 1
Adrian
  • 1,595
  • 1
  • 19
  • 21
  • This worked great! Was using a grouped table view and had to make everything relative to the cell in interface builder instead of to how it looked visually, but it worked to get it to indent and clip properly! – Kudit Jan 05 '13 at 05:27
  • +1 Added this to my base `UITableViewCell` so I'll never have to deal with this again. – memmons Mar 29 '13 at 00:51
  • The solution works for me but I had to do one change elsewhere. Instead of `[cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];` I call `[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];` – dmitri May 01 '13 at 07:28
  • Perfect! This does exactly what IB should be doing. – Pier-Luc Gendreau Jul 01 '13 at 23:34
9

Here is a subclass, based on other answers ideas, I'm going to base my custom cells on:

@interface FixedTableViewCell ()

- (void)initFixedTableViewCell;

@end

@interface FixedTableViewCell : UITableViewCell

@end

@implementation FixedTableViewCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (nil != (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
        [self initFixedTableViewCell];
    }
    return self;
}

- (void)awakeFromNib {
    [super awakeFromNib];

    [self initFixedTableViewCell];
}

- (void)initFixedTableViewCell {
    for (NSInteger i = self.constraints.count - 1; i >= 0; i--) {
        NSLayoutConstraint *constraint = [self.constraints objectAtIndex:i];

        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;

        BOOL shouldMoveToContentView = YES;

        if ([firstItem isDescendantOfView:self.contentView]) {
            if (NO == [secondItem isDescendantOfView:self.contentView]) {
                secondItem = self.contentView;
            }
        }
        else if ([secondItem isDescendantOfView:self.contentView]) {
            if (NO == [firstItem isDescendantOfView:self.contentView]) {
                firstItem = self.contentView;
            }
        }
        else {
            shouldMoveToContentView = NO;
        }

        if (shouldMoveToContentView) {
            [self removeConstraint:constraint];
            NSLayoutConstraint *contentViewConstraint = [NSLayoutConstraint constraintWithItem:firstItem
                                                                                     attribute:constraint.firstAttribute
                                                                                     relatedBy:constraint.relation
                                                                                        toItem:secondItem
                                                                                     attribute:constraint.secondAttribute
                                                                                    multiplier:constraint.multiplier
                                                                                      constant:constraint.constant];
            [self.contentView addConstraint:contentViewConstraint];
        }
    }
}

@end
Mihaylov
  • 91
  • 1
  • In my opinion, there is no use in calling initFixedTableViewCell from initWithStyle:reuseIdentifier: as it is a Interface Builder bug it cannot occur only if you load the cel from a nib file. Hopefully in code, one should not reproduce the Interface Builder's bug ;-) I also don't see why check for descendants in the constraints list. – Adrian Dec 20 '12 at 16:00
  • 2
    This worked well for me but you need to preserve the constraint priority too: "contentViewConstraint.priority = constraint.priority" – Justin Driscoll Feb 19 '13 at 19:06
6

An alternative to subclassing is to revise the constraints in cellForRowAtIndexPath.

Embed all the content of the cell inside a container view. Then point the leading and trailing constraints to the cell.contentView rather than the table view cell.

  UIView *containerView = [cell viewWithTag:999];
  UIView *contentView = [cell contentView];

  //remove existing leading and trailing constraints
  for(NSLayoutConstraint *c in [cell constraints]){
    if(c.firstItem==containerView && (c.firstAttribute==NSLayoutAttributeLeading || c.firstAttribute==NSLayoutAttributeTrailing)){
      [cell removeConstraint:c];
    }
  }

  NSLayoutConstraint *trailing = [NSLayoutConstraint
                                 constraintWithItem:containerView
                                 attribute:NSLayoutAttributeTrailing
                                 relatedBy:NSLayoutRelationEqual
                                 toItem:contentView
                                 attribute:NSLayoutAttributeTrailing
                                 multiplier:1
                                 constant:0];

  NSLayoutConstraint *leading = [NSLayoutConstraint
                                 constraintWithItem:containerView
                                 attribute:NSLayoutAttributeLeading
                                 relatedBy:NSLayoutRelationEqual
                                 toItem:contentView
                                 attribute:NSLayoutAttributeLeading
                                 multiplier:1
                                 constant:0];

  [cell addConstraint:trailing];
  [cell addConstraint:leading];
railwayparade
  • 5,154
  • 1
  • 39
  • 49
2

I think this is fixed in iOS 7 beta 3 making the workarounds unnecessary from that point on (but probably harmless as in most cases they will become empty operations).

Joseph Lord
  • 6,446
  • 1
  • 28
  • 32
1

Based on the code by Skoota (I am a beginner, don't know much of what you did, but excellent work) my suggestion is to put all your stuff in an edge-to-edge container view and add the following:

In the cell's header file, I have the following IBOutlets:

@property (weak, nonatomic) IBOutlet UIView *container;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leftConstrain;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *rightConstrain;

In the implementation file, I have the following in awakeFromNib:

// Remove the IB added horizontal constraint, as that's effective gainst the cell not the contentView
[self removeConstraint:self.leftConstrain];
[self removeConstraint:self.rightConstrain];

// Create a dictionary to represent the view being positioned
NSDictionary *containerViewDictionary = NSDictionaryOfVariableBindings(_container);

// Create the new left constraint (0 spacing because of the edge-to-edge view 'container')
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-0-[_container]" options:0 metrics:nil views:containerViewDictionary];
// Add the left constraint against the contentView
[self.contentView addConstraints:constraints];

// Create the new constraint right (will fix the 'Delete' button as well)
constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"[_container]-0-|" options:0 metrics:nil views:containerViewDictionary];
// Add the right constraint against the contentView
[self.contentView addConstraints:constraints];

Again, the above was made possible by Skoota. Thanks!!! Al credits go to him.