Reputation: 2973
I'm ashamed to post this, but I'm becoming desperate.
I'm a noob when it comes to AutoLayout. That's mostly because my app has nearly 60 different screens in it. Why change what's working? Anyway, I don't use XIBs/NIBs/Storyboards, because anytime I'd need to make an application-wide UI change, I'd have to fix a bunch of things. Instead, I have my own set of UIViewController subclasses. One of them just has a UITableView much like UITableViewController. I can crank out UITableViews in my sleep.
I have a new screen that needs a couple of buttons inside the table view's header. I'm trying to get better with AutoLayout, so I try to use it whenever I can. Without further narrative, here's a very short self-contained example.
#import "AppDelegate.h"
@interface TestViewController : UIViewController<UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) NSArray *rows;
@end
@implementation TestViewController
- (void) loadView {
UITableView *tv = [[UITableView alloc] initWithFrame:[[UIScreen mainScreen] bounds] style:UITableViewStyleGrouped];
[tv setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
[tv setDelegate:self];
[tv setDataSource:self];
[self setView:tv];
UIView *tableHeader = [[UIView alloc] init];
[tableHeader setTranslatesAutoresizingMaskIntoConstraints:NO];
[tv setTableHeaderView:tableHeader];
UIButton *selectProfileButton = [UIButton buttonWithType:UIButtonTypeCustom];
[selectProfileButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[selectProfileButton setShowsTouchWhenHighlighted:YES];
[selectProfileButton setTitle:@"Select Profile..." forState:UIControlStateNormal];
[tableHeader addSubview:selectProfileButton];
UIButton *editProfileButton = [UIButton buttonWithType:UIButtonTypeCustom];
[editProfileButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[editProfileButton setImage:[UIImage imageNamed:@"EditAccessoryIcon"] forState:UIControlStateNormal];
[tableHeader addSubview:editProfileButton];
UIEdgeInsets padding = UIEdgeInsetsMake(5, 10, 5, 5);
// select button in NW corner
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:selectProfileButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeTop multiplier:1.0 constant:padding.top]];
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:selectProfileButton attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeLeft multiplier:1.0 constant:padding.left]];
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:selectProfileButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-padding.bottom]];
// edit button in NE corner
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:editProfileButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeTop multiplier:1.0 constant:padding.top]];
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:editProfileButton attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeRight multiplier:1.0 constant:-padding.right]];
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:editProfileButton attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual
toItem:tableHeader attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-padding.bottom]];
// spacing between buttons
[tableHeader addConstraint:[NSLayoutConstraint constraintWithItem:selectProfileButton attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual
toItem:editProfileButton attribute:NSLayoutAttributeLeft multiplier:1.0 constant:-10.0]];
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[self rows] count]; }
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
}
[[cell textLabel] setText:[[self rows] objectAtIndex:[indexPath row]]];
return cell;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
TestViewController *test = [[TestViewController alloc] init];
[test setRows:@[@"1", @"2", @"3"]];
[[self window] setRootViewController:test];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
Unfortunately, at runtime I get this:
2014-07-11 13:18:48.502 TestTableHeader[9478:60b] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794
2014-07-11 13:18:48.505 TestTableHeader[9478:60b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'
*** First throw call stack:
(
0 CoreFoundation 0x017ed1e4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x0156c8e5 objc_exception_throw + 44
2 CoreFoundation 0x017ed048 +[NSException raise:format:arguments:] + 136
3 Foundation 0x0114c4de -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 116
4 UIKit 0x0029ba38 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 567
5 libobjc.A.dylib 0x0157e82b -[NSObject performSelector:withObject:] + 70
6 QuartzCore 0x03c5845a -[CALayer layoutSublayers] + 148
7 QuartzCore 0x03c4c244 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380
8 QuartzCore 0x03c583a5 -[CALayer layoutIfNeeded] + 160
9 UIKit 0x0035dae3 -[UIViewController window:setupWithInterfaceOrientation:] + 304
10 UIKit 0x00273aa7 -[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 5212
11 UIKit 0x00272646 -[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:] + 82
12 UIKit 0x00272518 -[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 117
13 UIKit 0x002725a0 -[UIWindow _setRotatableViewOrientation:duration:force:] + 67
14 UIKit 0x0027163a __57-[UIWindow _updateToInterfaceOrientation:duration:force:]_block_invoke + 120
15 UIKit 0x0027159c -[UIWindow _updateToInterfaceOrientation:duration:force:] + 400
16 UIKit 0x002722f3 -[UIWindow setAutorotates:forceUpdateInterfaceOrientation:] + 870
17 UIKit 0x002758e6 -[UIWindow setDelegate:] + 449
18 UIKit 0x0034fb77 -[UIViewController _tryBecomeRootViewControllerInWindow:] + 180
19 UIKit 0x0026b474 -[UIWindow addRootViewControllerViewIfPossible] + 591
20 UIKit 0x0026b5ef -[UIWindow _setHidden:forced:] + 312
21 UIKit 0x0026b86b -[UIWindow _orderFrontWithoutMakingKey] + 49
22 UIKit 0x002763c8 -[UIWindow makeKeyAndVisible] + 65
23 TestTableHeader 0x00002dfa -[AppDelegate application:didFinishLaunchingWithOptions:] + 890
24 UIKit 0x0022614f -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 309
25 UIKit 0x00226aa1 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1810
26 UIKit 0x0022b667 -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 824
27 UIKit 0x0023ff92 -[UIApplication handleEvent:withNewEvent:] + 3517
28 UIKit 0x00240555 -[UIApplication sendEvent:] + 85
29 UIKit 0x0022d250 _UIApplicationHandleEvent + 683
30 GraphicsServices 0x037e2f02 _PurpleEventCallback + 776
31 GraphicsServices 0x037e2a0d PurpleEventCallback + 46
32 CoreFoundation 0x01768ca5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
33 CoreFoundation 0x017689db __CFRunLoopDoSource1 + 523
34 CoreFoundation 0x0179368c __CFRunLoopRun + 2156
35 CoreFoundation 0x017929d3 CFRunLoopRunSpecific + 467
36 CoreFoundation 0x017927eb CFRunLoopRunInMode + 123
37 UIKit 0x0022ad9c -[UIApplication _run] + 840
38 UIKit 0x0022cf9b UIApplicationMain + 1225
39 TestTableHeader 0x00002fed main + 141
40 libdyld.dylib 0x01e34701 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
From what I've read, this means I'm missing a constraint. UIButtons have an intrinsic size, so I only need to set their position. The container UIView will be sized by the UITableView, so there's nothing to do there. (I even tried setting the height for giggles--I didn't giggle.)
Where have I screwed up?
Thanks!
Updated example code and stack to provide short self-contained example.
Upvotes: 2
Views: 734
Reputation: 7944
I had the same problem. I think it's not caused by missing constraints. It's rather because UITableView
apparently doesn't handle headers / footers with autolayout properly. The workaround that works for me is to set frame using systemLayoutSizeFittingSize:
method:
//initialize tableHeader, add subviews and constraints
CGSize size = [tableHeader systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
tableHeader.frame = (CGRect){.origin = CGPointZero, .size = size};
self.tableView.tableHeaderView = tableHeader;
Note that I didn't set translatesAutoresizingMaskIntoConstraints
to NO
.
Also, it seems you haven't added all the needed constraints. You only position the buttons, but your tableHeader doesn't know how to calculate its own width and height. You probably need to make selectProfileButton
's (or the other button's) bottom equal to tableHeader
's bottom and add a (minimum) horizontal spacing between buttons.
Also, as far as I remember, UITableView will extend tableHeader
's width to its own regardless of any constraints.
Upvotes: 2