bmauter
bmauter

Reputation: 2973

How do I get this simple layout for UITableView header to work using AutoLayout?

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

Answers (1)

Michał Ciuba
Michał Ciuba

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

Related Questions