I'm working on an application that uses today's widget where I need to show some table view with nearly 50 rows.but the screen fits only for 10 rows.So I need to increase the height of widget as per my table height.I've done a lot of research on this which says me can't be done.I've seen the yahoo stocks app,which has "show all" feature to display all stocks on widget with height more than that of screen height.If something is done somewhere why can't I do that? I've tried to set the height of my todayviewcontroller view height in both the ways using autolayout,setting "preferredContentSize".I really wanted to know whether I'm doing wrong somewhere, or it is not possible to have widget height more than screen height.Any suggestion is appreciated.
Here is my code Todayviewcontroller.m
-(void)adjustWidgetHeight {
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:self.view
attribute:NSLayoutAttributeHeight
relatedBy:0
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:2140];
heightConstraint.priority = 999;
[self.view addConstraint:heightConstraint];
[self.view needsUpdateConstraints];
[self.view setNeedsLayout];
}
The height can be dynamically changed, but I think it's not possible to reach a over-screen height.
the stock app is a preset app, so maybe can do that.
after I checked my widget demo code(I do some researches that how to create widget all by codes), i think it can not be done.
system will add a force constraint of height that exactly eqauls the widget panel ( in Iphone 5 & 5S , it's about 441), even I manually set 3000 height, it's still limited to 441.
you can check the demo gif:
it's my code for the test( I'm using Masonry to do autolayout)
//
// TodayViewController.m
// widget
//
// Created by Ralph Li on 4/29/15.
// Copyright (c) 2015 LJC. All rights reserved.
//
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>
#import <Masonry/Masonry.h>
#interface TodayViewController () <NCWidgetProviding>
#property (strong, nonatomic) UIView *contentView;
#property (nonatomic, strong) UIButton *btnTest;
#end
#implementation TodayViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.contentView = [UIView new];
self.contentView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
make.height.mas_equalTo(200).priorityHigh();
}];
self.btnTest = [UIButton buttonWithType:UIButtonTypeCustom];
[self.btnTest setTitle:[[NSDate date] description] forState:UIControlStateNormal];
self.btnTest.backgroundColor = [UIColor redColor];
[self.btnTest addTarget:self action:#selector(actionTest) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.btnTest];
[self.btnTest mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.contentView);
make.size.mas_equalTo(CGSizeMake(300, 40));
}];
}
- (void) actionTest
{
[self.contentView mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(#(self.contentView.frame.size.height>250?200:3000)).priorityHigh();
}];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResultFailed
// If there's no update required, use NCUpdateResultNoData
// If there's an update, use NCUpdateResultNewData
completionHandler(NCUpdateResultNewData);
}
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsZero;
}
#end
Related
My ViewController looks like this when initially loaded:
When show button is tapped it must change like this:
my code is:
- (IBAction)show:(id)sender {
[self change];
[tableView reloadData];
}
- (IBAction)close:(id)sender {
[self change];
[tableView reloadData];
}
-(void)change{
//assigning initial bounds to frame-1
CGRect frame1=CGRectMake(0, _pastProcessTV.frame.origin.y, self.view.bounds.size.width, self.pastProcessTV.bounds.size.height);
//assigning new bounds to frame-2
CGRect frame2=CGRectMake(0, **CGFloat y**,self.view.bounds.size.width,***CGFloat height***);
if (_showFullButton.isTouchInside) {
tableView.frame = frame2;
}
else{
tableview.frame = frame1;
}
}
I tried different ways. it is not working.can anyone help me in giving Y-coordinate and width
As you are using autolayout you have to take outlet of topconstraint of the tableview and when you want to expand it set its value to 0
So it will work as expected
if you are using iOS 7
[self.view layoutIfNeeded] is compulsary
If you have to use auto layout, please try like this picture
Make Layout Constraint Property
And try this code
#import "ViewController.h"
#interface ViewController ()
{
CGFloat oldtableViewTopValue;
}
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *tableViewTopLC;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
oldtableViewTopValue = _tableViewTopLC.constant;
}
- (IBAction)showBtnAction:(id)sender
{
[UIView animateWithDuration:0.5f animations:^{
_tableViewTopLC.constant = (_tableViewTopLC.constant==oldtableViewTopValue ? 0 : oldtableViewTopValue);
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
}
#end
Please turn off autolayout from Storyboard
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'm trying to position a button so that it is always an X amount of pixels from the bottom of the screen. The view is laid out programmatically using auto layout. If I use: #"V:|-44-[closeModalButton]" the object aligns 44 pixels from the top of the screen as expected. However, #"V:[closeModalButton]-44-|" causes the button to never show up. It seems like it ought to be pretty straightforward, so I feel like I'm missing something really obvious.
Implementation of view
#import "DetailView.h"
#implementation DetailView
#synthesize closeModalButton;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
CGRect aFrame = [[UIScreen mainScreen] bounds];
self.frame = aFrame;
closeModalButton = [UIButton buttonWithType:UIButtonTypeCustom];
[closeModalButton setImage: [UIImage imageNamed:#"closeButton.png"] forState:UIControlStateNormal];
[closeModalButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:closeModalButton];
NSDictionary *viewArranging = NSDictionaryOfVariableBindings(closeModalButton);
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[closeModalButton(44)]-44-|"
options:0
metrics:0
views:viewArranging]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-149-[closeModalButton(44)]-149-|"
options:0
metrics:0
views:viewArranging]];
}
return self;
}
#end
View controller
#import "DetailViewController.h"
#interface DetailViewController ()
#end
#implementation DetailViewController
{
DetailView *_detailView;
}
- (void)loadView
{
_detailView = [[DetailView alloc] init];
[self setView:_detailView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[_detailView.closeModalButton addTarget:self
action:#selector(closeModalButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)closeModalButtonPressed:(UIButton *)sender{
[self dismissViewControllerAnimated:YES
completion:nil];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Ok, so it looks like DetailView.h subclassed UIScrollView rather than UIView. Setting the proper subclass caused the constraints to operate as expected.
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.