contentView not indenting in iOS 6 UITableViewCell prototype cell
I am configuring a custom UITableViewCell
using a prototype cell in a Storyboard. However, all the UILabel
s (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.
Solution 1:
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.
Solution 2:
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];
}
}
Solution 3:
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