I have a custom UIView which I want to be able to reuse throughout my application as a section header in tableviews. Here is the applicable code for that (this is basically all it is doing):
-(id) initWithTitle:(NSString *)title{
self = [super init];
if(self){
_title = title;
self.translatesAutoresizingMaskIntoConstraints = false;
[self addSubview:self.titleLabel];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"H:|-%#-[_titleLabel]-%#-|", HORIZONTAL_SPACE, HORIZONTAL_SPACE] options:0 metrics:nil views:NSDictionaryOfVariableBindings(self, _titleLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"V:|-%#-[_titleLabel]-%#-|", VERTICAL_SPACE, VERTICAL_SPACE] options:0 metrics:nil views:NSDictionaryOfVariableBindings(self, _titleLabel)]];
[self updateConstraintsIfNeeded];
}
return self;
}
I am using it like this:
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section
{
NSString *headerText = _headers[section];
if(headerText.length){
AWSmallTextSectionHeader *header = [[AWSmallTextSectionHeader alloc] initWithTitle:headerText];
return header;
}
return [UIView new];
}
The problem is that when the views draw to the screen they all start at the same origin...This is what is looks like at the top of the tableview:
It is really weird though because it is acting as if it is in the correct section, for example, if you start scrolling and one of the sections go offscreen, the view that corresponds to that section header will disappear.
I have a feeling it has to do with the way I am adding my constraints but I am not sure.
Thanks
Edit (this is also implemented)
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section
{
if (section == SECTION_INDEX_PHONE) {
return 60.f; //CGRectGetHeight(self.phoneSectionHeader.frame);
} else if (section == SECTION_INDEX_FEEDBACK) {
return 40.f; //CGRectGetHeight(self.feedbackSectionHeader.frame);
}
return 5.0;
}
Should you at some point set the bounds for the header section view?
Also ensure you're adapting the heightForHeaderInSection:
This method only works correctly when tableView:heightForHeaderInSection: is also implemented.
self.translatesAutoresizingMaskIntoConstraints = false; was the problem.
Once I got rid of that it was working correctly.
Related
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'm working on a project that must support both iOS 8 and iOS 7.1. Right now I'm running into a problem that only appears on iOS 7.1 but works properly on iOS 8. I have a ViewController that contains a tableview and a custom view in the tableHeaderView. I'll post the code as follows. All constraints are added programatically.
//View Controller.
-(void)viewDidLoad
{
[super viewDidLoad];
self.commentsArray = [NSMutableArray new];
[self.commentsArray addObject:#"TEST"];
[self.commentsArray addObject:#"TEST"];
[self.commentsArray addObject:#"TEST"];
[self.commentsArray addObject:#"TEST"];
[self.commentsArray addObject:#"TEST"];
[self.commentsArray addObject:#"TEST"];
[self.view addSubview:self.masterTableView];
self.masterTableView.tableHeaderView = self.detailsView;
[self.view setNeedsUpdateConstraints];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.view layoutIfNeeded];
}
//Table view getter
- (UITableView *)masterTableView
{
if(!_masterTableView)
{
_masterTableView = [UITableView new];
_masterTableView.translatesAutoresizingMaskIntoConstraints = NO;
_masterTableView.backgroundColor = [UIColor greyColor];
_masterTableView.delegate = self;
_masterTableView.dataSource = self;
_masterTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_masterTableView.showsVerticalScrollIndicator = NO;
_masterTableView.separatorInset = UIEdgeInsetsZero;
}
return _masterTableView;
}
-(void)updateViewConstraints
{
NSDictionary *views = #{
#"table" : self.masterTableView,
#"details" : self.detailsView,
};
NSDictionary *metrics = #{
#"width" : #([UIScreen mainScreen].applicationFrame.size.width)
};
//Table view constraints
[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]];
//Details View
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[details(width)]-0-|" options:0 metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[details]-0-|" options:0 metrics:metrics views:views]];
}
//Details View Getter
- (DetailsView *)detailsView
{
if(!_detailsView)
{
_detailsView = [[DetailsView alloc]initWithFrame:CGRectMake(0, 0, 0, 100)];
_detailsView.backgroundColor = [UIColor orangeColor];
return _detailsView;
}
Now the details view contains some basic subviews which all derive from a UIView and the details view itself derives from a more general super class. I'll post the code as follows.
//Parent View
#interface ParentView (): UIView
- (instancetype)init
{
self = [super init];
if (self)
{
[self setupViews];
}
return self;
}
- (void)setupViews
{
[self addSubview:self.publishedTimeView];
}
- (void)updateConstraints
{
NSDictionary *views = #{
#"time" : self.publishedTimeView,
};
// Header with published video time
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[time]-10-|" options:0 metrics:0 views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[time(11)]" options:0 metrics:0 views:views]];
[super updateConstraints];
}
//Getter for the timePublished view added to the detail view. Will not post all its related code for //the sake of brevity.
- (TimePublishedDetailView *)publishedTimeView
{
if(!_publishedTimeView)
{
_publishedTimeView = [TimePublishedDetailView new];
_publishedTimeView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _publishedTimeView;
}
//Child View (or the detailsView) of the view controller.
#interface DetailsView : ParentView
#implementation RecordingDetailsView
- (void)updateConstraints
{
NSDictionary *views = #{
#"time" : self.publishedTimeView,
};
//Vertical alignment of all views
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[time]" options:0 metrics:nil views:views]];
[super updateConstraints];
}
- (void)setModel:(DetailViewModel *)model
{
self.publishedTimeView.date = model.dateTime;
[self setNeedsUpdateConstraints];
[self layoutSubviews];
}
Now on iOS 8 this looks like this:
However on iOS 7.1 this will crash with this error message:
"Exception: Auto layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super"
I've googled this and played around with the code in my layout calls to see if I can remedy the problem but so far have been unsuccessful. If anyone could post some tips or advice on how to fix this I would really appreciate it.
I know this question is old, but I ran into a similar problem recently and after a lot of Googling, trying and failing I made it work with the help of this answer and a few changes.
Just add this category to your project and call [UITableView fixLayoutSubviewsMethod]; only once (I recommend inside AppDelegate).
#import <objc/runtime.h>
#import <objc/message.h>
#implementation UITableView (FixUITableViewAutolayoutIHope)
+ (void)fixLayoutSubviewsMethod
{
Method existing = class_getInstanceMethod(self, #selector(layoutSubviews));
Method new = class_getInstanceMethod(self, #selector(_autolayout_replacementLayoutSubviews));
method_exchangeImplementations(existing, new);
}
- (void)_autolayout_replacementLayoutSubviews
{
[super layoutSubviews];
[self _autolayout_replacementLayoutSubviews]; // not recursive due to method swizzling
[super layoutSubviews];
}
#end
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.
I want to modify the section header view when user scrolls down, something similar to this in Music app
(Notics how the view background color has changed and got a bottom border)
Is there a good way to track when the view is on top of the section or in scrolling position?
Update:
My only solution so far is to keep an array of all the section header views and change the view of the first visible section in scrollViewDidScroll: delegate method (getting the first visible section index using tableView.indexPathsForVisibleRows array)
If anyone can come up with a simpler way, that would be great!
You can modify the color (and whatever else you want) of the section header view in the scrollViewDidScroll method. This example darkens the color of the floating header view as the user scrolls down, and keeps that color's white value between 0.9 and 0.6. It also unhides a bottom border line in the header view if you scroll down by more than 5 points.
The .m file for RDHeaderView:
- (id)init{
self = [super init];
if (self) {
UIView *line = [[UIView alloc] init];
[line setTranslatesAutoresizingMaskIntoConstraints:NO];
line.backgroundColor = [UIColor darkGrayColor];
[self addSubview:line];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[line]|" options:0 metrics:nil views:#{#"line":line}]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[line]|" options:0 metrics:nil views:#{#"line":line}]];
[line addConstraint:[NSLayoutConstraint constraintWithItem:line attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:1]];
self.bottomLine = line;
self.bottomLine.hidden = YES;
}
return self;
}
The relevant methods in the table view controller:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
RDHeaderView *header = [[RDHeaderView alloc] init];
header.contentView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
return header;
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSInteger topSection = [[self.tableView indexPathsForVisibleRows].firstObject section];
NSInteger sectionYOffset = [self.tableView rectForHeaderInSection:topSection].origin.y;
RDHeaderView *pinnedHeader = (RDHeaderView *)[self.tableView headerViewForSection:topSection];
pinnedHeader.bottomLine.hidden = ((scrollView.contentOffset.y - sectionYOffset) > 5)? NO: YES;
CGFloat colorOffset = fmaxf(0.6, 0.9 - (scrollView.contentOffset.y - sectionYOffset)/1000.0);
if (colorOffset > 0.9) colorOffset = 0.9;
pinnedHeader.contentView.backgroundColor = [UIColor colorWithWhite:colorOffset alpha:1];
}
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 80;
}
Maybe a simpler solution exists, but this woud be one way of achieving what you want:
remove the tableHeader, and add it as a separate subview to your viewControllers view (N.B. do not use a uitableviewController, as that viewController has the tableview as its view, and you don't want that)
move the tableView down so that it fits right under that former-tableheader view
compute and set the contentOffset of the tableView (it is a UIScrollView) so that the cells don't seem to jump to a new position.
Also, you could try the return a sectionHeaderView (which scrolls along with the tableView down, but not up, see for example in you contacts list how this works). Play around with that view being a cell or a section header.
I have a problem with a custom view and autolayout. To make things simple I will use two UILabels, the first one should change its background color when the device rotate. The problem is that it doesn't do it! Any hint?
Thanks!
Nicola
- (id)init
{
self = [super init];
if (self) {
//Add the subviews to the mainView
[self.view addSubview:self.label1];
[self.view addSubview:self.label2];
//Autolayout
//Create the views dictionary
NSDictionary *viewsDictionary = #{#"header":self.label1,
#"table": self.label2};
//Create the constraints using the visual language format
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat: #"H:|[header]|"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat: #"H:|[table]|"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[header(==50)][table]|"
options:0
metrics:nil
views:viewsDictionary]];
}
return self;
}
-(UIView*) label1
{
_label1 = [UILabel alloc] init];
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)){
_label1.backgroundColor = [UIColor redColor];
}else{
_label1.backgroundColor = [UIColor greenColor];
}
_label1.translatesAutoresizingMaskIntoConstraints=NO;
return _label1;
}
-(UIView*) label2
{
_label2 = [UILabel alloc] init];
_label2.backgroundColor = [UIColor yellowColor];
_label2.translatesAutoresizingMaskIntoConstraints=NO;
_return label2;
}
-(BOOL) shouldAutorotate
{
return YES;
}
-(NSUInteger) supportedInterfaceOrientations
{
if([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad){
//I am on a pad
return UIInterfaceOrientationMaskAll;
} else {
//I am on a Phone
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//I expect the label1 to change its background color
[self.view setNeedDisplay];
}
If you move the code related to [self.label1 setBackgroundColor:] to the delegate method didRotateFromInterfaceOrientation:, it should work better. Also, in your custom getters, you are allocating a new label every time you access the method. In most situations it's preferable to check if the ivar is not nil at the beginning, and returning the ivar, instead for allocating a fresh label.