I'm using Xcode 4.6.1 and I have an iOS 6.1 app.
It has a table view that drills down into another table view. The second table view uses custom cells to display its data. However, there is a problem with the second table view: its content size is usually wrong (specifically the height, which is either too tall or too short). This either results in there being extra space at the bottom, or there not being enough space to be able to scroll to the bottom.
The picture below shows four screenshots from a test app that I made which reproduces the problem. I'm going to give a walk-through of the steps leading to my problem.
The app loads, and the first table is shown. It has three rows that drill down into the second table.
I select the first row, which is "List: 0 to 10". The navigation controller pushes the second table onto the screen. It has has 11 rows, and (correctly) has the content size for 11 rows.
Then I went back and selected the next row, which is "List: A to P". The second table comes onto the screen again, and it has 16 rows now. However, it only has the content size for 11 rows. The other 5 rows are still down below, but the content size keeps bouncing me back up to the 11th row.
Then I went back and selected the last row, which is "List: a to f". The second table comes onto the screen again, and it has 6 rows. However, it still has the content size for 11 rows. There is too much space at the bottom of the table view, and it shows 5 cells at the bottom with nothing in them.
The pattern here is that the content size stays at whatever it was when it initially loaded. So the first row I select will have the correct content size, but all the other ones will now stay at that size, regardless of how many rows they have. This also means if I had selected "List: a to f" first, which has 6 rows, then all the other ones would have a content size for 6 rows, because it stays at whatever the content size was for the first row I selected.
In my code, I am never changing the content size myself; this is all happening automatically. To figure out when and why the table view was changing its content size, I set an observer on the second table's "contentSize" property. After I did this, I realized that the content size wasn't staying the same, but that it was actually being changed twice when I selected a row: first to the correct size (when the table view reloaded), and then reverted back to the old size (when the table view appeared).
So I'm trying to figure out why it's being reverted back to the old size. To see what was going on behind the scenes, I had it print out a stack trace every time it observed the content size changing.
Below, you can see the console log for the previous steps that a walked you through. However, to conserve space and make it more readable, I replaced the long stack traces with a message like, "(Stack Trace Type A)". I'll show you the details of them later, but for now I'm just calling them Stack Trace Type A, B, or C.
first table: selected row 0 (List: 0 to 10)
second table: viewDidLoad
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type A)
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type B)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear
first table: selected row 1 (List: A to P)
OBSERVED CHANGE: contentSize.height = 704 (row count = 16)
(Stack Trace Type A)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 16)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear
first table: selected row 2 (List: a to f)
OBSERVED CHANGE: contentSize.height = 264 (row count = 6)
(Stack Trace Type A)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 6)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear
As you can see, it's reverting the content size back to the old size. In this case, that would be 484, which is enough space to show 11 cells with a height of 44. If things were working correctly, the observed change that's coupled with Stack Trace Type C would not be happening. So in other words, Type C is what's causing the problem.
Now I'll show you what the stack traces actually look like. Note that the first two (A and B) aren't related to the problem. I'm showing them only so that you can compare/contrast them with Type C, which is the one that seems to cause the problem.
(Stack Trace Type A) When the table view changed its content size due to being reloaded, the stack trace looked like this.
(
0 CustomTableCellTest 0x00002fe0 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
1 Foundation 0x00b0d417 NSKeyValueNotifyObserver + 357
2 Foundation 0x00b26b24 NSKeyValueDidChange + 456
3 Foundation 0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
4 Foundation 0x00b48b80 _NSSetSizeValueAndNotify + 185
5 UIKit 0x000b57ef -[UITableView(_UITableViewPrivate) _updateContentSize] + 782
6 UIKit 0x000c3974 -[UITableView noteNumberOfRowsChanged] + 154
7 UIKit 0x000c32dc -[UITableView reloadData] + 769
8 CustomTableCellTest 0x00004d97 -[MetaMasterViewController tableView:didSelectRowAtIndexPath:] + 567
9 UIKit 0x000c7285 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1194
10 UIKit 0x000c74ed -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
11 Foundation 0x00ad15b3 __NSFireDelayedPerform + 380
12 CoreFoundation 0x01c55376 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
13 CoreFoundation 0x01c54e06 __CFRunLoopDoTimer + 534
14 CoreFoundation 0x01c3ca82 __CFRunLoopRun + 1810
15 CoreFoundation 0x01c3bf44 CFRunLoopRunSpecific + 276
16 CoreFoundation 0x01c3be1b CFRunLoopRunInMode + 123
17 GraphicsServices 0x01bf07e3 GSEventRunModal + 88
18 GraphicsServices 0x01bf0668 GSEventRun + 104
19 UIKit 0x00017ffc UIApplicationMain + 1211
20 CustomTableCellTest 0x000021ed main + 141
21 CustomTableCellTest 0x00002115 start + 53
)
(Stack Trace Type B) This one occurs only the first time the table view appears, so it only happens once. I believe it occurs when the table initially loads its view.
(
0 CustomTableCellTest 0x00002fe0 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
1 Foundation 0x00b0d417 NSKeyValueNotifyObserver + 357
2 Foundation 0x00b26b24 NSKeyValueDidChange + 456
3 Foundation 0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
4 Foundation 0x00b48b80 _NSSetSizeValueAndNotify + 185
5 UIKit 0x000b57ef -[UITableView(_UITableViewPrivate) _updateContentSize] + 782
6 UIKit 0x000cbc8e -[UITableView _rectChangedWithNewSize:oldSize:] + 261
7 UIKit 0x000cc231 -[UITableView setFrame:] + 279
8 UIKit 0x000f5014 +[UIViewControllerWrapperView wrapperViewForView:frame:] + 448
9 UIKit 0x00110e18 -[UINavigationController _startTransition:fromViewController:toViewController:] + 239
10 UIKit 0x0011189b -[UINavigationController _startDeferredTransitionIfNeeded:] + 386
11 UIKit 0x00111e93 -[UINavigationController pushViewController:transition:forceImmediate:] + 1030
12 UIKit 0x00111a88 -[UINavigationController pushViewController:animated:] + 62
13 CustomTableCellTest 0x00004e21 -[MetaMasterViewController tableView:didSelectRowAtIndexPath:] + 705
14 UIKit 0x000c7285 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1194
15 UIKit 0x000c74ed -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
16 Foundation 0x00ad15b3 __NSFireDelayedPerform + 380
17 CoreFoundation 0x01c55376 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
18 CoreFoundation 0x01c54e06 __CFRunLoopDoTimer + 534
19 CoreFoundation 0x01c3ca82 __CFRunLoopRun + 1810
20 CoreFoundation 0x01c3bf44 CFRunLoopRunSpecific + 276
21 CoreFoundation 0x01c3be1b CFRunLoopRunInMode + 123
22 GraphicsServices 0x01bf07e3 GSEventRunModal + 88
23 GraphicsServices 0x01bf0668 GSEventRun + 104
24 UIKit 0x00017ffc UIApplicationMain + 1211
25 CustomTableCellTest 0x000021ed main + 141
26 CustomTableCellTest 0x00002115 start + 53
)
So Type A and B both occur when things are working correctly. However, when things are not working correctly, I also get this next stack trace.
(Stack Trace Type C) This is the stack trace that occurs when the content size is reverted back to the old size.
(
0 CustomTableCellTest 0x00003080 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
1 Foundation 0x00b0d417 NSKeyValueNotifyObserver + 357
2 Foundation 0x00b26b24 NSKeyValueDidChange + 456
3 Foundation 0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
4 Foundation 0x00b48b80 _NSSetSizeValueAndNotify + 185
5 UIKit 0x0007c213 -[UIScrollView _resizeWithOldSuperviewSize:] + 161
6 UIKit 0x0005cf2a -[UIView(Geometry) resizeWithOldSuperviewSize:] + 72
7 UIKit 0x0005bb28 __46-[UIView(Geometry) resizeSubviewsWithOldSize:]_block_invoke_0 + 80
8 CoreFoundation 0x01cb85a7 __NSArrayChunkIterate + 359
9 CoreFoundation 0x01c9003f __NSArrayEnumerate + 1023
10 CoreFoundation 0x01c8fa16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
11 UIKit 0x0005babf -[UIView(Geometry) resizeSubviewsWithOldSize:] + 149
12 UIKit 0x00557dcc -[UIView(AdditionalLayoutSupport) _is_layout] + 143
13 UIKit 0x000607ae -[UIView(Hierarchy) layoutSubviews] + 80
14 UIKit 0x000682dd -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 279
15 libobjc.A.dylib 0x010e76b0 -[NSObject performSelector:withObject:] + 70
16 QuartzCore 0x02292fc0 -[CALayer layoutSublayers] + 240
17 QuartzCore 0x0228733c _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 468
18 QuartzCore 0x02287150 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 26
19 QuartzCore 0x022050bc _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 324
20 QuartzCore 0x02206227 _ZN2CA11Transaction6commitEv + 395
21 QuartzCore 0x022068e2 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 96
22 CoreFoundation 0x01c5eafe __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 30
23 CoreFoundation 0x01c5ea3d __CFRunLoopDoObservers + 381
24 CoreFoundation 0x01c3c7c2 __CFRunLoopRun + 1106
25 CoreFoundation 0x01c3bf44 CFRunLoopRunSpecific + 276
26 CoreFoundation 0x01c3be1b CFRunLoopRunInMode + 123
27 GraphicsServices 0x01bf07e3 GSEventRunModal + 88
28 GraphicsServices 0x01bf0668 GSEventRun + 104
29 UIKit 0x00017ffc UIApplicationMain + 1211
30 CustomTableCellTest 0x000021ed main + 141
31 CustomTableCellTest 0x00002115 start + 53
)
As you can see, there is some QuartzCore stuff happening that wasn't present in Type A or B. Also, on several of the lines in Type C, there are actually methods being called behind the scenes with the words "OldSize" in their names. So apparently it really is reverting it back the "old size". However, I still don't understand why these QuartzCore and "OldSize" methods are being called in the first place.
I tried looking up "resizeSubviewsWithOldSize", but the only documentation I could find on it was for NSView (which is for OS X apps, not iOS apps which use UIView). This description in the docs didn't really help me understand why it was being called in this case.
However, even though I don't know why it's being called, I have a pretty good idea of when it's being called. With more testing I realized that it reverts to the old content size whenever the table view appears, and sometimes when it disappears.
For example, when I made a modal view cover the screen and then go away, the content size was actually changed 3 times. The log messages looked something like this.
*Modal view entering*
second table: viewWillDisappear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidDisappear
*Modal view exiting*
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
So the change isn't related to the table view being reloaded: it has to do with it appearing and disappearing. I'm guessing that views try to layout their subviews every time they appear, so there seems to be a problem with how my custom cells are laid out.
Initially I thought that the problem was with how my custom cells were set up: either in my code or in the xib. This was because the problem disappeared when I didn't use my custom cells. But then one time I turned auto layout off in my custom cell xib, and the problem also disappeared. So apparently the problem has something to do with auto layout.
For the sake of easy debugging, the way I set up the cell in the xib is very simple: the only thing on the cell is a UILabel with a gray background color. All of the auto layout constraints were the default ones made by Interface Builder. You can see what my custom cell xib looks like below.
I also tried deleting the UILabel so the cell had nothing on it, and I left auto layout turned on. When I did this, the problem also went away. So I guess there's something wrong with the way the UILabel is set up on the cell. Which is odd since it was set up with those "purple" default constraints by Interface Builder that I can't delete.
After looking around for information regarding auto layout in table view cells, I came across this question. Apparently, Interface Builder doesn't set its constraints to the cell's content view correctly. I wasn't sure if this was related to my problem, but I tried it anyways. I used the solution posted by Adrian, which looked like this.
// MasterCell.m
// My custom UITableViewCell subclass
- (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];
}
}
However, while it seemed to do what it was intended to do (i.e. change the constraints to be related to the content view instead of the cell itself), this still didn't solve my problem: the content size was still being reverted back to the old size.
So in conclusion, and to clarify this rather long question, this is basically what I'm asking.
Why is it that, when I enable auto layout in my custom table cell xib, that the table view's content size gets incorrectly re-sized back to the "old size" when the table appears on the screen?
You are re-using the same instance of a table view controller for the "Master" table, but you are sending it messages in what appears to be the wrong order.
You push the controller like this:
if (!self.masterViewController) {
self.masterViewController = [[MasterViewController alloc] init];
}
self.masterViewController.objects = self.masterObjects[indexPath.row];
[self.masterViewController.tableView reloadData];
[self.navigationController pushViewController:self.masterViewController animated:YES];
If you simply swap around the reloadData and push lines, then your problem disappears.
I'm not entirely sure what is happening here, but I think the problem is that, once the master controller is popped, it doesn't have any frame of reference (pun intended!) to calculate the appropriate content size when you reload it, so it probably isn't caching the content size for this new height.
Then, when the view is pushed, the scroll view suddenly has a context again, it knows it has recalculated and so uses the cached content size. That is the impression I get (as you did) from the stack trace, anyway. The first table you choose is always fine since there isn't an old size available. If you'd made this with storyboards you would be instantiating a new Master controller each time and you'd never have noticed a problem.
As for why autolayout makes any difference, well, again I'm only guessing and hand-waving here, but under autolayout everything happens a lot later on in the view controller lifecycle than it does under explicit layout. If you have autolayout all the way down, then in a view with no superview (and therefore no constraints on its size) autolayout is not able to work out what size things should be. It's probably a bug, but the workaround is trivial enough and as I say, typically when pushing new view controllers onto a navigation stack you'd initialise new ones, and they'd go away when they were popped, so you'd never see this.
User contributions licensed under CC BY-SA 3.0