I'm currently manually positioning my UI elements, which gets annoying if I update the text or add elements as I have to recalculate all the positions each time.
Is there a way using autolayout to size and positions the elements, for example as follows, no matter how much text the labels contain?
UILabel (multiline, variable height)
[20px gap]
UILabel (multiline, variable height)
[20px gap]
UIButton
Yes, it will look something like this
#implementation MyClass {
NSDictionary *_viewsDictionary;
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// First create your controls - you can just use CGRectZero
_label1 = [[UILabel alloc] initWithFrame:CGRectZero];
_label1 setText:#"Some text";
_label2 = [[UILabel alloc] initWithFrame:CGRectZero];
_label2 setText:#"Some text 2";
_button = [[UIButton alloc] initWithFrame:CGRectZero];
// Then just add them to your as a sub view
[self addSubview:self.currentBalanceLabel];
[self addSubview:_nameLabel];
[self addSubview:_button];
// Put them in an NSDictionary - this is a macro and will be used when setting up the contraints below
_viewsDictionary = NSDictionaryOfVariableBindings(_nameLabel, _currentBalanceLabel,_button);
// This tells the view to run update contraints
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
}
- (void)updateConstraints
{
[super updateConstraints];
// Using the _viewsDictionary, we must tell always tell how all the controls will be setup both
// horizontally and vertically. In this case wear are going to tell the label to take the entire width
// The rest of the vies will be aligned on the left below
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_label1]-|"
options:0
metrics:nil
views:_viewsDictionary];
//Add the contraints
[self addConstraints:constraints];
// Next setup the vertical contraints. This is what you asked about spefically, label - 20 - label - 20 - button
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[_labelq]-20-[_label2]-20-[_button]"
options: NSLayoutFormatAlignAllLeft
metrics:nil
views:_viewsDictionary];
[self addConstraints:constraints];
}
#end
Related
Using Interface Builder, I have built a really long ScrollView filled with Custom UIViews, regular UIViews, StackViews, UILabels, UIButtons, etc.
For some of the Custom UIViews, if they do not have any data, then I want to replace them with a UILabel that says "No Data Available" and I want to be able to set the margins and center the text of that UILabel.
What's the best/easiest way to do this programmatically in my ViewController given that all the views are arranged using interface builder?
Thanks for your help in advance!
You can do this by adding a UILabel, with some simple constraints, over the views you want to cover instead of inside them if you want to ensure you aren't messing with controls you don't, well, control.
I set up a simple test app to show how this method can work
This has a stack view with some images in it, a text view, and a button to trigger the sample.
You should be able to apply this method to your views as you determine in your code that you have no data to show, and want to show the placeholder, but in my example I've set up an IBOutletCollection that has both the stack view and the text view in it, and am running this on both views when the button is pressed.
All you need to do is provide the placeholder text and the view you want to replace to this method
/// This method will hide a view and put a placeholder label in that view's superview, centered in the target view's frame.
- (void)showPlaceholderText:(NSString *)placeholder forView:(UIView *)view
{
// Build the placeholder with the same frame as the target view
UILabel *placeholderLabel = [[UILabel alloc] initWithFrame:view.frame];
placeholderLabel.textAlignment = NSTextAlignmentCenter;
placeholderLabel.text = placeholder;
placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO;
// Hide the target view
view.hidden = YES;
// Put our placeholder into the superview, overtop the target view
[view.superview addSubview:placeholderLabel];
// Set up some constraints to ensure the placeholder label stays positioned correctly
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeRight multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]];
[view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:placeholderLabel attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f]];
}
The constraints added to the placeholder should keep it positioned correctly, through rotation or any other layout activity in the view.
One idea is, instead of replacing the custom views with labels, give them an "noData" mode where they present the right thing if there's no data...
// CustomView.h
#interface CustomView : UIView
#property(assign,nonatomic) BOOL noData;
#end
// CustomView.m
#interface CustomView ()
#property(weak,nonatomic) UILabel *noDataLabel;
#end
- (void)setNoData:(BOOL)noData {
_noData = noData;
self.noDataLabel.alpha = (noData)? 1.0 : 0.0;
}
- (UILabel *)noDataLabel {
if (!_noDataLabel) {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:self.bounds];
noDataLabel.backgroundColor = self.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self addSubview:noDataLabel];
_noDataLabel = noDataLabel;
}
return _noDataLabel;
}
EDIT
If you want to treat the custom views as untouchable, you can handle the state in the view controller that contains them, but it's a little awkward because we need to solve the problem of associating the noData label with the subview. Something like this can work...
// in the view controller that contains the views that should be covered with labels
#interface ViewController ()
#property(weak,nonatomic) NSMutableArray *noDataViews;
#end
// initialize noDataViews early, like in viewDidLoad
_noDataViews = [#[] mutableCopy];
The array noDataViews can contain dictionaries. The dictionary will contain the view that has noData (this can be an instance of your third-party custom view), and a UILabel intended to cover it.
- (void)setView:(UIView *)view hasNoData:(BOOL)noData {
// find the dictionary corresponding to view
NSDictionary *dictionary;
for (NSDictionary *d in self.noDataViews) {
if (d[#"view"] == view) {
dictionary = d;
break;
}
}
// if it doesn't exist, insert it
if (!dictionary) {
UILabel *label = [self labelToCover:view];
dictionary = #{ #"view":view, #"label":label };
[self.noDataViews addObject:dictionary];
}
// get the label
UILabel *label = dictionary[#"label"];
label.alpha = (noData)? 1.0 : 0.0;
}
// create a label that will cover the passed view, add it as a subview and return it
- (UILabel *)labelToCover:(UIView *)view {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:view.frame];
noDataLabel.backgroundColor = view.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self.view addSubview:noDataLabel];
return noDataLabel;
}
Depending on how often the views change state to the noData state, you might want to clean up the dictionaries, removing those whose label's alpha == 0.0.
- (void)releaseNoDataViews {
NSMutableArray *removeThese = [#[] mutableCopy];
// work out which ones to remove
for (NSDictionary *d in self.noDataViews) {
UILabel *label = d[#"label"];
if (label.alpha == 0.0) {
[removeThese addObject:d];
}
}
for (NSDictionary *d in removeThese) {
UILabel *label = d[#"label"];
[label removeFromSuperview];
[self.noDataViews removeObject:d];
}
}
This a little verbose because by keeping our hands off the custom views, we put the logic to change how they look (cover them) in the view controller.
Maybe a better idea that keeps hands off the custom views is to wrap them in a containing view that does the additional work adding the noData state.
For example, say CustomView comes from the third party. Create a class called CustomViewWrapper that contains the CustomView as a child and adds the noData behavior outlined above. Instead of painting CustomViews in IB, paint CustomViewWrappers....
// CustomViewWrapper.h
#class CustomView;
#interface CustomViewWrapper : UIView
#property(assign,nonatomic) BOOL noData;
#end
// CustomViewWrapper.m
#import "CustomView.h"
#interface CustomViewWrapper ()
#property(weak,nonatomic) CustomView *customView;
#property(weak,nonatomic) UILabel *noDataLabel;
#end
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecorder];
if (self) {
CustomView *customView = [[CustomView alloc] init];
[self addSubView:customView];
_customView = customView;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.customView.frame = self.bounds;
}
- (void)setNoData:(BOOL)noData {
_noData = noData;
self.noDataLabel.alpha = (noData)? 1.0 : 0.0;
}
- (UILabel *)noDataLabel {
if (!_noDataLabel) {
UILabel *noDataLabel = [[UILabel alloc] initWithFrame:self.bounds];
noDataLabel.backgroundColor = self.backgroundColor;
noDataLabel.textAlignment = NSTextAlignmentCenter;
noDataLabel.text = #"NO DATA";
// configure font, etc.
[self addSubview:noDataLabel];
_noDataLabel = noDataLabel;
}
return _noDataLabel;
}
I have a custom table view cell, with an additional text field text view.
I've added in autolayout constraints. The cell's text label layout seems to be fine in portrait mode, but when rotated, the text label moves to the center of the cell(behind the text view), it is until I tap and type into the text view, the label gets adjusted to the right location.
.h
#interface MTTextViewCell : UITableViewCell
- (id)initWithLabelTextForView:(NSString *)labelTextForView;
#property (nonatomic, strong) UITextView *cellTextView;
#end
.m
#import "MTTextViewCell.h"
#implementation MTTextViewCell
- (id)initWithLabelTextForView:(NSString *)labelTextForView {
self = [super init];
if(self) {
//1. Setup existing cell elements.
self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.textLabel.text = labelTextForView;
self.selectionStyle = UITableViewCellSelectionStyleNone;
//2. Setup custom cell elements.
_cellTextView = [[UITextView alloc] init];
_cellTextView.translatesAutoresizingMaskIntoConstraints = NO;
_cellTextView.backgroundColor = [UIColor grayColor];
[self.contentView addSubview:_cellTextView];
self.autoresizesSubviews = YES;
//Local variables for the purposes of autolayout language and macros.
UIView *alTextLabel = self.textLabel;
UIView *alCellTextView = _cellTextView;
NSDictionary *alViewDictionary = NSDictionaryOfVariableBindings(alTextLabel, alCellTextView);
//Horizontal layout constraints.
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[alCellTextView]-|"
options:0
metrics:nil
views:alViewDictionary]];
//Vertical layout constraints.
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[alTextLabel(20.0)]-[alCellTextView]-|"
options:NSLayoutFormatAlignAllLeft
metrics:nil
views:alViewDictionary]];
}
return self;
}
This also sporadically happens to a similar cell using a text field, as
well as a segmented control.
See screencap:
I am on Xcode Version 6.4, iOS SDK 8.4
Any suggestions or pointers is appreciated! Thanks.
check that the heights add up to the maximum height of the phone in landscape mode. It's probably adding to greater than the height now
I'm running into a problem setting a tableHeaderView for a UITableView. I would like to have a detailsView be set as the tableHeaderView. The height of this detailsView will vary slightly and is not known immediately in view did load. The detailsView also has it's own subViews that have their own auto layout constraints. All auto layout is being done programatically. Let me post some sample code:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.tableView];
self.tableView.tableHeaderView = self.detailsView;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self loadDetails];
}
-(void)loadDetails
{
//Omitted but does the following:
//1. Makes a call to the api to get details
//2. Once received sets the details to the detailsView
//3. Details could vary which influences detailView height size.
}
- (DetailsView *)detailsView
{
if(!_detailsView)
{
__weak DetailedViewController *_self = self;
_detailsView = [DetailsView new];
_detailsView.backgroundColor = [UIColor greenColor];
_detailsView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _detailsView;
}
-(void)updateViewConstraints
{
NSDictionary *views = #{
#"table" : self.tableView,
};
//Comment Detail View
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[table]-0-|" options:0 metrics:0 views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[table]-0-|" options:0 metrics:0 views:views]];
}
The problem with this approach is the tableHeaderView is pushed up to the top, meaning I can't scroll all the way through it properly. I'm not sure why this is happening. What I did as a test was replaced the detailsView with a UIImageView as follows.
-(UIImageView *)testImageView
{
if(!_testImageView)
{
NSString *filePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"blackImage"] ofType:#"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
_testImageView = [[UIImageView alloc]initWithImage:image];
_testImageView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _testImageView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.view addSubview:self.tableView];
self.tableView.tableHeaderView = self.testImageView;
}
This code produces exactly what I need without knowing the height or any of the dimensions of the image.
As evidence by the photo, the header view is fully viewable and scrollable and I did not need to know any size of the image for this to work. I'd like to achieve the same thing with a UIView instead of the UIImageView for the tableHeaderView.
Important Notes:
The details view is not added as a sub view
The details view has not vertical height constraints or any constraints computed
The details view sub views have constraints computed programatically
Things I've Researched:
I've looked into instrinsicSize, sizeThatFits, anything that would allow a UIView to fill up the parent container view (tableHeaderView). I've tried various combinations of things with no success.
If anyone has a solution for how to solve this problem I'd appreciate it greatly!
(Let me know if this is not enough code to convey the context of the problem and I will post more.)
Use
systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
0) Init your header generically, being sure that the constraints hit all four sides of the headerView's frame.
1) Set the frame of the header view using
systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
2) Assign your header view to the tableview's frame
For example:
self.headerView = [[YourCustomHeaderView alloc] initWithYourCustomObject:obj];
self.headerView.frame = CGRectMake(0,0, SCREEN_WIDTH, [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height);
self.tableView.tableHeaderView = self.headerView;
I have an horizontal scroll view on which i add views dynamically.
On LTR languages everything work fine, i add views one after the other from left to right.
On RTL the problem is that the views always added to the left of the scroll instead of to the right like in every other controller, the really strange staff that the order of the views is added correctly, to the left of the first view so they are ordered from right to left but outside of the scroll view on -x.
Here is my code when i add a new View:
Tag* tag = [self.storyboard instantiateViewControllerWithIdentifier:#"tag" ];
[_scroller addSubview:tag.view];
[tags addObject:tag];
Tag* prev = nil
for (Tag* tag in tags)
{
if (prev == nil)
{
[_scroller addConstraint:[NSLayoutConstraint constraintWithItem:tag.view
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:_scroller
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0]];
}
else
{
[_scroller addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[prev]-10-[tag]"
options:0
metrics:nil
views:#{#"tag" : tag.view, #"prev" : prev.view}]];
}
[_scroller addConstraint:[NSLayoutConstraint constraintWithItem:tag.view
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_scroller
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0]];
prev = tag;
}
Here is an image of how it suppose to work on LTR and RTL and how it actually works
The reason for this behavior of UIScrollView is that you forgot to attach the trailingAnchor of the last element (#4) to the scroll view's trailingAnchor.
The leadingAnchor of both the scroll view and element #1 are attached to each other (see below in green). The scroll view's content rect however naturally spans into the positive coordinate directions, from origin (0,0) to right, down (+x, +y). In your case the scroll view's content size is of width 0 because nothing is between scroll view's leadingAnchor and trailingAnchor.
So below your [_scroller addConstraints:_constraint]; add something like (pseudo code):
if tag == lastTag {
NSLAyoutconstraints.activate([
tag.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
])
}
It sounds like a better approach might be to use a UICollectionView. Then if you want to start it from the right side you could possibly do something like this:
NSIndexPath *lastIndex = [NSIndexPath indexPathForItem:data.count - 1
inSection:0];
[self.collectionView scrollToItemAtIndexPath:lastIndex
atScrollPosition:UICollectionViewScrollPositionRight
animated:NO];
This way the UICollectionViewFlowLayout can handle the placement for you.
Try this
#import "ViewController.h"
#interface ViewController ()<UIScrollViewDelegate>
{
UIView *baseView;
UILabel *titleLabel;
NSMutableArray *infoArray ;
UIScrollView *mainscrollview;
}
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
infoArray =[[NSMutableArray alloc]initWithObjects:#"1",#"2",#"3", nil];
NSLog(#"%#",infoArray);
mainscrollview=[[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 380)];
mainscrollview.delegate=self;
mainscrollview.contentSize=CGSizeMake(320*infoArray.count, 0);
[self.view addSubview:mainscrollview];
[self sscrollcontent:#"LTR"];//LTR for Lefttoright other than LTR it will show RTL
}
-(void)sscrollcontent:(NSString *)flowtype
{
int xaxis=0;
for (int i=0; i<infoArray.count; i++) {
baseView=[[UIView alloc]initWithFrame:CGRectMake(xaxis, 0, 320, 380)];
[mainscrollview addSubview:baseView];
titleLabel =[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 320, 60)];
titleLabel.textAlignment=NSTextAlignmentCenter;
if ([flowtype isEqualToString:#"LTR"]) {
titleLabel.text=infoArray[i];
}
else
{
titleLabel.text=infoArray[infoArray.count-i-1];
}
[baseView addSubview:titleLabel];
xaxis=xaxis+320;
}
}
#end
Hope this will help you
This is my sample code.
//
// ViewController.m
// testConstraint
//
// Created by stevenj on 2014. 3. 24..
// Copyright (c) 2014년 Steven Jiang. All rights reserved.
//
#import "ViewController.h"
#interface TagView : UILabel
- (void)setNumber:(NSInteger)num;
#end
#implementation TagView
- (void)setNumber:(NSInteger)num
{
[self setText:[NSString stringWithFormat:#"%d",num]];
}
#end
#interface ViewController ()
#property (nonatomic, strong) UIScrollView *scroller;
#property (nonatomic, strong) NSMutableArray *tags;
#property (nonatomic, strong) NSMutableArray *constraint;
#end
#implementation ViewController
#synthesize scroller = _scroller;
- (void)viewDidLoad
{
[super viewDidLoad];
_tags = [NSMutableArray new];
_constraint = [NSMutableArray new];
// Do any additional setup after loading the view, typically from a nib.
//step.1 create scroll view
_scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 20, 320, 60)];
[_scroller setBackgroundColor:[UIColor lightGrayColor]];
[_scroller removeConstraints:[_scroller constraints]];
[_scroller setTranslatesAutoresizingMaskIntoConstraints:YES];
[self.view addSubview:_scroller];
//step.2 add tag view
for (int i=0; i<10; i++) {
TagView *tag = [[TagView alloc] init];
[tag setFrame:CGRectMake(100, 30, 50, 30)];
[tag setNumber:i];
[tag.layer setBorderWidth:1.0];
[tag setTranslatesAutoresizingMaskIntoConstraints:NO];
[_scroller addSubview:tag];
[_tags addObject:tag];
}
//step.3 update contraints
[self myUpdateConstraints];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)myUpdateConstraints
{
[_constraint removeAllObjects];
TagView* prev = nil;
for (TagView* tag in _tags)
{
[tag setNumber:[_tags indexOfObject:tag]];
if (prev == nil)
{
[_constraint addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(<=300)-[tag]-20-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:#{#"tag" : tag}]];
}
else
{
[_constraint addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"[tag]-10-[prev]"
options:0
metrics:nil
views:#{#"tag" : tag, #"prev" : prev}]];
}
[_scroller addConstraint:[NSLayoutConstraint constraintWithItem:tag
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_scroller
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0]];
prev = tag;
}
[_scroller addConstraints:_constraint];
}
#end
Hopes it could help you.
I am working with a UITableViewController. I have a table of items that the user can delete if he goes into edit more. When he goes into edit mode, I want to show a header that gives an option to delete all items. At the same time, it should show a label giving information about how much space is being used. I want this to automatically resize if the device goes into landscape mode. From what I can tell, I need to use autolayout to do this.
I would have loved to set up the header in a UIView designed in the Storyboard, but the Storyboard only allows view controllers, not views. I know I could have a XIB file hold it, but I would rather avoid that if I could.
To start with, I've overridden the editing property so that I can redraw the table section when in editing mode.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
NSIndexSet *set = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationAutomatic];
}
I use this code to insert the section header when appropriate:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
else
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView].frame.size.height;
else
return 0;
}
The magic happens in the - headerView method. It returns a UIView *, getting it from a cache if necessary. It adds the button and the label and then puts in the constraints. I've used these same constraints in the Storyboard and I haven't had any problems.
- (UIView *)headerView
{
if (headerView)
return headerView;
float w = [[UIScreen mainScreen] bounds].size.width;
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
CGRect deleteAllButtonFrame = CGRectMake(8.0, 8.0, 30.0, 30); // The autolayout should resize this.
[deleteAllButton setFrame:deleteAllButtonFrame];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
CGRect textFrame = CGRectMake(47.0, 8.0, 30.0, 30); // The autolayout should resize this.
UILabel *currSizeText = [[UILabel alloc] initWithFrame:textFrame];
currSizeText.text = #"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
CGRect headerViewFrame = CGRectMake(0, 0, w, 48);
headerView = [[UIView alloc] initWithFrame:headerViewFrame];
//headerView.autoresizingMask = UIViewAutoresizingNone;//UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//headerView.translatesAutoresizingMaskIntoConstraints = NO;
[headerView addSubview:deleteAllButton];
[headerView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
[headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[deleteAllButton]-[currSizeText]-|"
options:0
metrics:nil
views:viewsDictionary]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerView;
}
Right now, everything is working beautifully. The button keeps a constant size (because the hugging and compression resistance are higher than the label's) and the label will change its text to fit the available space. It resizes when I rotate the device. The vertical centering seems off on the label, but I am willing to overlook that for now.
However, when I first setup the section header, I get an annoying autolayout warning.
2014-02-07 11:25:19.770 ErikApp[10704:70b] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0xb9a4ad0 H:|-(NSSpace(20))-[UIButton:0xb99e220] (Names: '|':UIView:0xb9a4680 )>",
"<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>",
"<NSLayoutConstraint:0xb9a4c20 H:[UILabel:0xb99f530]-(NSSpace(20))-| (Names: '|':UIView:0xb9a4680 )>",
"<NSAutoresizingMaskLayoutConstraint:0xa2d1680 h=--& v=--& H:[UIView:0xb9a4680(0)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
My first thought was to change the returned UIView property translatesAutoresizingMaskIntoConstraints to NO. When I do that, I get a crash instead of a warning. Not exactly an improvement.
2014-02-07 10:49:13.041 ErikApp[10597:70b] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2903.23/UIView.m:8540
2014-02-07 10:49:13.383 ErikApp[10597:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'
Does anyone have a suggestion as to what to do to get rid of the warning?
It seems that when your section is reloading, the UITableView at some moment has a reference to both the old section header and the new one. And if it is the same view, some issues appear. So you must always provide a different view from the tableView:viewForHeaderInSection: method.
Sometimes it is really useful to have a single instance to be presented in a section header. For this purpose you need to create a new view each time you are asked for a section header and put your custom view inside it, configuring constraints appropriately. Here's an example:
#property (strong, nonatomic) UIView *headerContentView;
- (void)viewDidLoad {
// Create the view, which is to be presented inside the section header
self.headerContentView = [self loadHeaderContentView];
// Note that we have to set the following property to NO to prevent the unsatisfiable constraints
self.headerContentView.translatesAutoresizingMaskIntoConstraints = NO;
}
- (UIView *)loadHeaderContentView {
// Here you instantiate your custom view from a nib
// or create it programmatically. Speaking in terms
// of the OP, it should look like the following. (Note:
// I have removed all the frame-related code as your are
// not supposed to deal with frames directly with auto layout.
// I have also removed the line setting translatesAutoresizingMaskIntoConstraints property
// to NO of the headerContentView object as we do it explicitly in viewDidLoad.
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
UILabel *currSizeText = [[UILabel alloc] init];
currSizeText.text = #"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
UIView *headerContentView = [[UIView alloc] init];
[headerContentView addSubview:deleteAllButton];
[headerContentView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
// In the original post you used to have an ambigious layout
// as the Y position of neither button nor label was set.
// Note passing NSLayoutFormatAlignAllCenterY as an option
[headerContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[deleteAllButton]-[currSizeText]-|"
options:NSLayoutFormatAlignAllCenterY
metrics:nil
views:viewsDictionary]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
// Here setting the heights of the subviews
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerContentView;
}
- (UIView *)headerView {
UIView *headerView = [[UIView alloc] init];
[headerView addSubview:self.headerContentView];
NSDictionary *views = #{#"headerContentView" : self.headerContentView};
NSArray *hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[headerContentView]|" options:0 metrics:nil views:views];
NSArray *vConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[headerContentView]|" options:0 metrics:nil views:views];
[headerView addConstraints:hConstraints];
[headerView addConstraints:vConstraints];
return headerView;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// You need to return a concrete value here
// and not the current height of the header.
if (self.isEditing)
return 48;
return 0;
}
I created a GitHub repo for this post here:https://github.com/bilobatum/AnimatedTableHeaderDemo
This solution implements a table header view, i.e., self.tableView.tableHeaderView, instead of section headers for a table view with a single section.
The table header view and its subviews are colored for testing purposes. An arbitrary table header height is chosen for testing purposes.
The table header is lazily instantiated and animates into place when the table view enters editing mode. An animation hides the table header when the table view exits editing mode.
In general, you're not supposed to set frames when using Auto Layout. However, a table header is a special case in a sense. Don't use Auto Layout to size or position a table header. Instead, you must set a table header's frame (actually, you only need to set the rect's height). In turn, the system will translate the table header's frame into constraints.
However, it's okay to use Auto Layout on the table header's subviews. Some of these constraints are installed on the table header view.
#interface ViewController ()
#property (nonatomic, strong) NSArray *mockData;
#property (nonatomic, strong) UIButton *deleteAllButton;
#property (nonatomic, strong) UILabel *label;
#property (nonatomic, strong) UIView *headerView;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"Fruit";
self.mockData = #[#"Orange", #"Apple", #"Pear", #"Banana", #"Cantalope"];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (UIButton *)deleteAllButton
{
if (!_deleteAllButton) {
_deleteAllButton = [[UIButton alloc] init];
_deleteAllButton.backgroundColor = [UIColor grayColor];
[_deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
_deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[_deleteAllButton addTarget:self action:#selector(handleDeleteAll) forControlEvents:UIControlEventTouchUpInside];
}
return _deleteAllButton;
}
- (UILabel *)label
{
if (!_label) {
_label = [[UILabel alloc] init];
_label.backgroundColor = [UIColor yellowColor];
_label.text = #"Delete all button prompt";
_label.translatesAutoresizingMaskIntoConstraints = NO;
}
return _label;
}
- (UIView *)headerView
{
if (!_headerView) {
_headerView = [[UIView alloc] init];
// WARNING: do not set translatesAutoresizingMaskIntoConstraints to NO
_headerView.backgroundColor = [UIColor orangeColor];
_headerView.clipsToBounds = YES;
[_headerView addSubview:self.label];
[_headerView addSubview:self.deleteAllButton];
[_headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_deleteAllButton]-[_label]-|" options:NSLayoutFormatAlignAllCenterY metrics:0 views:NSDictionaryOfVariableBindings(_label, _deleteAllButton)]];
[_headerView addConstraint:[NSLayoutConstraint constraintWithItem:self.deleteAllButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_headerView attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]];
}
return _headerView;
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (self.editing) {
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
}
[UIView animateWithDuration:1.0 animations:^{
CGRect rect = self.headerView.frame;
if (editing) {
rect.size.height = 60.0f; // arbitrary; for testing purposes
} else {
rect.size.height = 0.0f;
}
self.headerView.frame = rect;
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
} completion:^(BOOL finished) {
if (!editing) {
self.tableView.tableHeaderView = nil;
}
}];
}
- (void)handleDeleteAll
{
NSLog(#"handle delete all");
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.mockData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = self.mockData[indexPath.row];
return cell;
}
#end
Quite a time since you asked the question, but maybe the answer is jet helpfull to you (or others).
Autolayout has (automatically) added a constraint for the whole section header width (the last in the debug output constrains list). This should of course be no problem, as the width is taken into account when calculation the frames of the subviews.
But sometimes there seem to be rounding errors in the calculation of the frames...
Just add a lower priority to one of the subviews width values to solve the problem:
...#"|-[deleteAllButton(30.0#999)]-[currSizeText]-|"
If the button width is not constant use ...deleteAllButton(>=30#999)...
The workaround that I've tried using is to skip the section header stuff and go directly to the tableHeaderView. I've replaced my editing property with this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (editing)
self.tableView.tableHeaderView = [self headerView];
else
self.tableView.tableHeaderView = nil;
}
It doesn't animate as nicely as the section header, but this will do for now.
This doesn't really address the actual problem (hence "workaround") so I won't accept this as the solution.