Custom UITableViewCell with drop-down in Swift - ios

I want to implement custom UITableViewCell with XIB. Here is the drawing. The button on the right side of the main view shows/hides additional view. I have two questions:
How can I hide the additional view? I think that one possibility would be to set height in the frame of additional UIView to zero. Are there any better options?
The buttons in additional view (in this case 1-5) should appear dynamically. There are two sets of data: one for the left side (buttons 1-3) and other one for the right side (buttons 4-5). The height for the left buttons is fixed, let's say 70px each. The height of the buttons on the right side should be adjusted so that total height of right side buttons is the same as total height on left side. How can the buttons be added dynamically considering these rules?
The buttons will be added during runtime. For example there are two arrays:
var leftButtons:[String] = ["button1label", “button2label“, "button3label"]
var rightButtons:[String] = ["button4label", "button5label"].
Let's say I add "button6label" to leftButtons later on during runtime. The size for the leftView/rightView as well as the size for the buttons inside these views should be adjusted. Again, height of each button on the left-hand size is fixed.

Firstly you should add ContainerView view on your custom UITableViewCell, Now add constraints on the ContainerView - leading , trailing, top & bottom to superView with priority 999 for all constraints`.
Now you should add two view on ContainerView, one is mainView and other is additionView.
and add constraints on the mainView - leading to superView, top to superView, trailing to superView and height constraint (let say 70).
now add textfield and show/hide button in side the mainView and apply constraints on textField and show/hide button.
textField constraints - leading to superView, top to superView, bottom to superView and Horizontal spacing between textField & show/hide button.
show/hide button constraints - top to superView, bottom to superView, trailing to superView and width constraints.
Here mainView is configured properly. So now let's configure additionView
you should add two new view in additionView, one is leftView and other is rightView & add constraints on the leftView & rightView.
leftView constraints - leading to superView, top to superView, bottom to superView , Horizontal spacing between leftView & rightView,
equal width and width constraints of leftView to rightView.
rightView constraints - trailing to superView, top to superView&bottom to superView
Here your Interface Builder Designing completed so now we need to manage the buttons on left and right View at runtime. to do this you must make a custome classs is called VerticalContainerView that will manage the buttons vertical distribution.
I have created the VerticalContainerView using the KVConstraintExtensionsMaster library to apply constraints that I have implemented.
Put below code in VerticalContainerView.h header file
#import <UIKit/UIKit.h>
#interface VerticalContainerView : UIView
-(void)configureButtonsbyNames:(NSArray<__kindof NSString *>*)names isDistribuated:(BOOL)isDistributed;
#end
Put below code in VerticalContainerView.m file
#import "VerticalContainerView.h"
#import "KVConstraintExtensionsMaster.h"
#implementation VerticalContainerView
-(void)configureButtonsbyNames:(NSArray<__kindof NSString *>*)names isDistribuated:(BOOL)isDistributed
{
/* Just Two steps to Apply\Add constraints by programatically */
/* Step 1 create & configure the view hierarchy for constraint first. */
/* Step 2 Apply the constraints */
CGFloat space = 0.0;
CGFloat height = 70.0;
UIButton *previousContentButton = nil;
NSInteger count = names.count;
for (NSInteger i = 0; i < count; i++)
{
UIButton *contentButton = [UIButton prepareNewViewForAutoLayout];
if (i&1) {
[contentButton setBackgroundColor:[UIColor greenColor]];
}else{
[contentButton setBackgroundColor:[UIColor purpleColor]];
}
[contentButton setTag:i];
[contentButton setTitle:names[i] forState:UIControlStateNormal];
[self addSubview:contentButton];
[contentButton applyLeadingAndTrailingPinConstraintToSuperviewWithPadding:space];
if (!isDistributed) {
[contentButton applyHeightConstraint:height];
}
if (i == 0) // for first
{
[contentButton applyTopPinConstraintToSuperviewWithPadding:space];
}
else if (i == count-1) // for last
{
if (isDistributed) {
[previousContentButton applyConstraintFromSiblingViewAttribute:NSLayoutAttributeHeight toAttribute:NSLayoutAttributeHeight ofView:contentButton spacing:space];
}
[previousContentButton applyConstraintFromSiblingViewAttribute:NSLayoutAttributeBottom toAttribute:NSLayoutAttributeTop ofView:contentButton spacing:space];
[contentButton applyBottomPinConstraintToSuperviewWithPadding:space];
}
else
{
if (isDistributed) {
[previousContentButton applyConstraintFromSiblingViewAttribute:NSLayoutAttributeHeight toAttribute:NSLayoutAttributeHeight ofView:contentButton spacing:space];
}
[previousContentButton applyConstraintFromSiblingViewAttribute:NSLayoutAttributeBottom toAttribute:NSLayoutAttributeTop ofView:contentButton spacing:space];
}
previousContentButton = contentButton;
}
}
#end
Now create a custom Cell is called CustomCell & put below code in CustomCell.h header file
#import "VerticalContainerView.h"
#interface CustomCell : UITableViewCell
#property (weak, nonatomic) IBOutlet VerticalContainerView *leftVerticalContainerView;
#property (weak, nonatomic) IBOutlet VerticalContainerView *rightVerticalContainerView;
#end
Put below code in CustomCell.m file.
#import "CustomCell.h"
#implementation CustomCell
-(void)prepareForReuse
{
// here you have to remove the all the buttons from left and right veiw becuase
// Every cell can have distinct number buttons on left and right view.
for (UIView *subView in self.leftVerticalContainerView.subviews) {
[subView removeFromSuperview];
}
for (UIView *subView in self.leftVerticalContainerView.subviews) {
[subView removeFromSuperview];
}
[super prepareForReuse];
}
#end
Now changes UITableViewCell Class by our CustomCell with the help of Identity inspector editor of Interface Builder
Also changes left and right View Class by our VerticalContainerView with the help of Identity inspector editor of Interface Builder
Now connect the IBOutlet of our CusromCell for leftVerticalContainerView & rightVerticalContainerView
Put the below code in the viewDidLoad method ofyour ViewController is:
tableView.rowHeight = UITableViewAutomaticDimension;
/* any estimated height but must be more than 2, but it should be more estimated */
tableView.estimatedRowHeight = 210.0;
tableView.delegate = self;
tableView.dataSource = self;
// if you created cell from `.xib` is called CustomCell.xib,then you have to register that cell with table.
// UINib *nib = [UINib nibWithNibName:#"CustomCell" bundle:nil];
// [tableView registerNib:nib forCellReuseIdentifier:#"YouCellIdentifier"];
Now implement UITableView DataSource in your ViewController
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection: (NSInteger)section{
return 10;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"CustomCell";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (indexPath.row%2 == 0) {
// this is fixed height constraints
[cell.leftVerticalContainerView configureButtonsbyNames:#[#"button1",#"button2",#"button3"] isDistributed:NO];
// this is distributed height constraints according to left view
[cell.rightVerticalContainerView configureButtonsbyNames:#[#"button4",#"button5"] isDistributed:YES];
}
else{
// this is fixed height constraints
[cell.leftVerticalContainerView configureButtonsbyNames:#[#"button1",#"button2",#"button3",#"button4",#"button5"] isDistributed:NO];
// this is isDistribuated height constraints according to left view
[cell.rightVerticalContainerView configureButtonsbyNames:#[#"button6",#"button7",#"button8"] isDistributed:YES];
}
return cell;
}

You can do this using UITableViewCells with Automatic Height.
This allows you to use constraints to determine the cell's height.
To get this working:
Add the following lines to turn on Automatic Height:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0 // An approximate cell height.
Use constraints to make the cell's content push outwards so that the content determines the cell height.
Add a height constraint to the additional view with an IBOutlet in code. Set this constraint's constant property to 0, and set the .hidden property of additional view to true to hide the additional view.
For a nicer opening/closing, change the constraints constant property inside a UIView animation block.

Related

Resize xib UIView to fit UIButton inside

I have a UIView created from a xib file. Inside that UIView there is a UIButton with a text that may be longer or shorter depending on the language.
What I want is to resize the parent UIView to fit the width of the UIButton. How can I do it with AutoLayout, or programmatically?
Thanks,
add leading, trailing, top and bottom constraint of button
add width constraint of view and connect to viewWidthConstraint.
calculate width of button for language and set constant of viewWidthConstraint.
View and button will resize
#property (nonatomic, weak) IBOutlet NSLayoutConstraints *viewWidthConstraint;
- (void)viewDidLoad {
super.viewDidLoad();
self.viewWidthConstraint.constant = [self buttonWidth];
}
- (CGFloat)buttonWidth {
return 100.f; (calculate width of button)
}

Tableheader view autolayout issue

I'm loading a custom UIView's XIB file as a header view of a uitableview inside a view controller.
The file owner for the xib file is the viewcontroller. I have both the viewcontroller's & the uiview's interface declared inside the uiviewcontroller.
ViewController.h
#class ZeroStateView;
#interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, weak) IBOutlet CustomUITableView *tableView;
#property (nonatomic,strong) NSMutableArray *dataArray;
#property (weak, nonatomic) IBOutlet ZeroStateView *zeroStateView;
#end
#interface ZeroStateView : UIView
#property (weak, nonatomic) IBOutlet AutoLayoutLabel *titleLabel;
#property (weak, nonatomic) IBOutlet UIImageView *titleIcon;
- (void)updateView;
#end
ViewController.m
- (void)prepareHeaderViewForZeroState{
ZeroStateView *sizingView = [ZeroStateView new];
[[[NSBundle mainBundle] loadNibNamed:#"ZeroStateView" owner:self options:nil] objectAtIndex:0];
sizingView = self.zeroStateView;
[sizingView updateView];
self.tableView.tableHeaderView = sizingView;
UIView *headerView = self.tableView.tableHeaderView;
CGFloat height = [headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = headerView.frame;
headerFrame.size.height = height;
headerView.frame = headerFrame;
self.tableView.tableHeaderView = headerView;
}
#end
#implementation ZeroStateView
-(void)updateView{
self.titleIcon.alpha = 0.5;
UIFontDescriptor *titleFontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleSubheadline];
self.titleLabel.text = #"This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. This is a long text message and its really long. ";
}
The AutolayoutLabel class the following method overridden:
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
// For multiline label, preferredMaxLayoutWidth always matches the frame width
if (self.numberOfLines == 0 && bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
[self setNeedsUpdateConstraints];
}
}
The height calculated by the systemLayoutSizeFittingSize:UILayoutFittingCompressedSize returns 0. As a result I get the following view as the table header view:
When I added the actual height as below, the uilabel overflows. I'm expecting the uiview to grow as the label height grows.
headerFrame.size.height = self.sizingView.frame.size.height;
Here is the screen capture of that UIViews constraints:
What do I miss here? Can someone point me out?
Update
I created a sample project for you guys to check on whats exactly issue is.
I reimplemented what you had so far in a different way. To start, I removed the UIView in ZeroStateView.xib that the UIImageView and UILabel were embedded in. The base of the xib is already a UIView, so it is unnecessary to add another UIView to it.
Next I changed the constraints around. I don't remember exactly what constraints I changed, so I will just list them out here:
Constraints for UIImageView
Align Center X to Superview
Width = 60
Height = 56
Top Space to Superview = 37
Bottom Space to UILabel = 31
Constraints for UILabel
Left Space to Superview = 15
Right Space to Superview = 15
Bottom Space to Superview = 45
Top Space to UIImageView = 31
Onto the code. In ViewController.h, the IBOutlet wasn't doing anything as far as I could tell, so I changed that property to read #property (strong, nonatomic) ZeroStateView *zeroStateView;
Now the important changes: ViewController.m. There are two UITableViewDelegate methods that will replace prepareHeaderViewForZeroState. In viewDidLoad, initialize the zeroStateView and set the table view's delegate to self.
- (void)viewDidLoad
{
//...
// Load the view
self.zeroStateView = [[[NSBundle mainBundle] loadNibNamed:#"ZeroStateView" owner:self options:nil] firstObject];
[self.zeroStateView updateView];
self.zeroStateView.backgroundColor = [UIColor darkGrayColor];
// Set self for table view delegate for -heightForHeaderInSection: and viewForHeaderInSection:
self.dataTable.delegate = self;
}
Now that we are the table view's delegate, we get two method calls that will allow us to customize the header view and set its height appropriately.
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// This will set the header view to the zero state view we made in viewDidLoad
return self.zeroStateView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// sizeToFit describes the size that the label can fit itself into.
// So we are saying that the label can use the width of the view and infinite height.
CGSize sizeToFit = CGSizeMake(self.view.frame.size.width, MAXFLOAT);
// Then we ask the label to tell us how it can fit in that size.
// The label will respond with the width of the view and however much height it needs
// to display its text. This is the magic of how it grows vertically.
CGSize size = [self.zeroStateView.titleLabel sizeThatFits:sizeToFit];
// Add the height the label needs to the overall zero state view. This should be changed
// to the height of the UIImage + the height we just got + the whitespace above and below
// each of these views. You can handle that part.
return self.zeroStateView.frame.size.height + size.height;
}
I uploaded my changes to Dropbox here.
Just changing the size of the frame of the tableHeaderView, doesn't change it's size. You have to set it again to the tableView to force a reload.
You have to call this again self.tableView.tableHeaderView = headerView; after setting the new frame size.
As you call prepareHeaderViewForZeroState method from viewwillappear. At that point you layout is not calculate. so force layout to calculate before calling systemLayoutSizeFittingSize method to calculate height of cell. Here are the code that you need to write before calling systemLayoutSizeFittingSize.
UIView *header = self.tableView.tableHeaderView;
[header setNeedsLayout];
[header layoutIfNeeded];
Edit :
You just left 1 constraints in ZeroStateView.xib. that is Bottom Space to : Superview. kindly refer screenshot.
Output :
Here you have Updated code
Hope this help you.
I'm not sure but you could check if you have a leading,trailing,top and bottom constraints for both the UIImage and the label with reference to the superview.
Edit:
Add the width constraint before getting the systemLayoutSize
NSLayoutConstraint *tempWidthConstraint =
[NSLayoutConstraint constraintWithItem:self.contentView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:CGRectGetWidth(window.frame)];
widthConstraint.constant = tempWidthConstraint.constant;
[self.contentView addConstraint:tempWidthConstraint];
CGSize fittingSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
CGFloat height = fittingSize.height +1;
[self.contentView removeConstraint:tempWidthConstraint];
There are a few things here that are confusing to me that might be complicating things.
Why are you subclassing the label?
The constraints don't really make sense.
Why does the label have two height constraints, a constant and a ≥ constraint? Remove them both, you don't need either.
Also, your vertical space to the bottom of the container is ≥ as well. Why? As it stands, your custom view doesn't have a defined height, which could be why the fitting size returns a height of zero.
Once you resolve those issues, let me know if you have better luck.

UITableCell clipToBounds property odd behavior

I have a custom UITableCell that has labels and a UIView.
Lets say the tablecell is set at a height of 50px and 10 labels are placed with 10px seperation. With cell.clipToBounds = YES, we only see 5 of the labels which is nice and expected.
The custom UIView on this custom table cell is say 200px in height. I ASSUMED the UIView would get clipped as the labels did because the UIView is added as a subview to the cell itself which clips everything else just fine but the UIVIew doesnt get clipped at all. The entire UIView (200px height) is shown.
pseudo code:
cellForRowAtIndexPath... ... {
...
...
CustomCell * cell = ... ...
cell.clipToBounds = YES;
cell.customView.hidden = NO;
[self.view addSubView:cell.customView];
[self.view bringSubviewToFront:cell.customView];
}
In storyboard i literally create a custom cell and add 10 labels as mentioned above and drag and drop a UIView in the cell with a large height.
So why don't views added to "content view" of UITableViewCell get clipped?
also set cell viewContent clipsToBound
cell.contentview.clipToBounds = YES;
and also check if the cells are overlapping, then make sure you are passing the right height in heightForRowAtIndexPath method of tableview.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 50;
}

How to resize superview to fit all subviews with autolayout?

My understanding of autolayout is that it takes the size of superview and base on constrains and intrinsic sizes it calculates positions of subviews.
Is there a way to reverse this process? I want to resize superview on the base of constrains and intrinsic sizes. What is the simplest way of achieving this?
I have view designed in Xcode which I use as a header for UITableView. This view includes a label and a button. Size of the label differs depending on data. Depending on constrains the label successfully pushes the button down or if there is a constrain between the button and bottom of superview the label is compressed.
I have found a few similar questions but they don’t have good and easy answers.
The correct API to use is UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize.
For a normal UIView using autolayout this should just work as long as your constraints are correct. If you want to use it on a UITableViewCell (to determine row height for example) then you should call it against your cell contentView and grab the height.
Further considerations exist if you have one or more UILabel's in your view that are multiline. For these it is imperitive that the preferredMaxLayoutWidth property be set correctly such that the label provides a correct intrinsicContentSize, which will be used in systemLayoutSizeFittingSize's calculation.
EDIT: by request, adding example of height calculation for a table view cell
Using autolayout for table-cell height calculation isn't super efficient but it sure is convenient, especially if you have a cell that has a complex layout.
As I said above, if you're using a multiline UILabel it's imperative to sync the preferredMaxLayoutWidth to the label width. I use a custom UILabel subclass to do this:
#implementation TSLabel
- (void) layoutSubviews
{
[super layoutSubviews];
if ( self.numberOfLines == 0 )
{
if ( self.preferredMaxLayoutWidth != self.frame.size.width )
{
self.preferredMaxLayoutWidth = self.frame.size.width;
[self setNeedsUpdateConstraints];
}
}
}
- (CGSize) intrinsicContentSize
{
CGSize s = [super intrinsicContentSize];
if ( self.numberOfLines == 0 )
{
// found out that sometimes intrinsicContentSize is 1pt too short!
s.height += 1;
}
return s;
}
#end
Here's a contrived UITableViewController subclass demonstrating heightForRowAtIndexPath:
#import "TSTableViewController.h"
#import "TSTableViewCell.h"
#implementation TSTableViewController
- (NSString*) cellText
{
return #"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}
#pragma mark - Table view data source
- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
return 1;
}
- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
return 1;
}
- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
static TSTableViewCell *sizingCell;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: #"TSTableViewCell"];
});
// configure the cell
sizingCell.text = self.cellText;
// force layout
[sizingCell setNeedsLayout];
[sizingCell layoutIfNeeded];
// get the fitting size
CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
NSLog( #"fittingSize: %#", NSStringFromCGSize( s ));
return s.height;
}
- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: #"TSTableViewCell" ];
cell.text = self.cellText;
return cell;
}
#end
A simple custom cell:
#import "TSTableViewCell.h"
#import "TSLabel.h"
#implementation TSTableViewCell
{
IBOutlet TSLabel* _label;
}
- (void) setText: (NSString *) text
{
_label.text = text;
}
#end
And, here's a picture of the constraints defined in the Storyboard. Note that there are no height/width constraints on the label - those are inferred from the label's intrinsicContentSize:
Eric Baker's comment tipped me off to the core idea that in order for a view to have its size be determined by the content placed within it, then the content placed within it must have an explicit relationship with the containing view in order to drive its height (or width) dynamically. "Add subview" does not create this relationship as you might assume. You have to choose which subview is going to drive the height and/or width of the container... most commonly whatever UI element you have placed in the lower right hand corner of your overall UI. Here's some code and inline comments to illustrate the point.
Note, this may be of particular value to those working with scroll views since it's common to design around a single content view that determines its size (and communicates this to the scroll view) dynamically based on whatever you put in it. Good luck, hope this helps somebody out there.
//
// ViewController.m
// AutoLayoutDynamicVerticalContainerHeight
//
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) UIView *contentView;
#property (strong, nonatomic) UILabel *myLabel;
#property (strong, nonatomic) UILabel *myOtherLabel;
#end
#implementation ViewController
- (void)viewDidLoad
{
// INVOKE SUPER
[super viewDidLoad];
// INIT ALL REQUIRED UI ELEMENTS
self.contentView = [[UIView alloc] init];
self.myLabel = [[UILabel alloc] init];
self.myOtherLabel = [[UILabel alloc] init];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);
// TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;
// ESTABLISH VIEW HIERARCHY
[self.view addSubview:self.contentView]; // View adds content view
[self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
[self.contentView addSubview:self.myOtherLabel];
// LAYOUT
// Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
// Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet
/* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
// ^To me this is what's weird... but okay once you understand...
// Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
// Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
// Note, this is the view that we are choosing to use to drive the height (and width) of our container...
// The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];
// Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];
// COLOR VIEWS
self.view.backgroundColor = [UIColor purpleColor];
self.contentView.backgroundColor = [UIColor redColor];
self.myLabel.backgroundColor = [UIColor orangeColor];
self.myOtherLabel.backgroundColor = [UIColor greenColor];
// CONFIGURE VIEWS
// Configure MY LABEL
self.myLabel.text = #"HELLO WORLD\nLine 2\nLine 3, yo";
self.myLabel.numberOfLines = 0; // Let it flow
// Configure MY OTHER LABEL
self.myOtherLabel.text = #"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
self.myOtherLabel.numberOfLines = 0;
self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}
#end
You can do this by creating a constraint and connecting it via interface builder
See explanation: Auto_Layout_Constraints_in_Interface_Builder
raywenderlich beginning-auto-layout
AutolayoutPG Articles constraint Fundamentals
#interface ViewController : UIViewController {
IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
connect this Constraint outlet with your sub views Constraint or connect super views Constraint too and set it according to your requirements like this
self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign
I hope this clarifies it.
This can be done for a normal subview inside a larger UIView, but it doesn't work automatically for headerViews. The height of a headerView is determined by what's returned by tableView:heightForHeaderInSection: so you have to calculate the height based on the height of the UILabel plus space for the UIButton and any padding you need. You need to do something like this:
-(CGFloat)tableView:(UITableView *)tableView
heightForHeaderInSection:(NSInteger)section {
NSString *s = self.headeString[indexPath.section];
CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17]
constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping];
return size.height + 60;
}
Here headerString is whatever string you want to populate the UILabel, and the 281 number is the width of the UILabel (as setup in Interface Builder)

How to use Auto Layout to move other views when a view is hidden?

I have designed my custom Cell in IB, subclassed it and connected my outlets to my custom class. I have three subviews in cell content which are: UIView (cdView) and two labels (titleLabel and emailLabel). Depending on data available for each row, sometimes I want to have UIView and two labels displayed in my cell and sometimes only two labels. What I am trying to do is to set constraints that way if I set UIView property to hidden or I will remove it from superview the two labels will move to the left. I tried to set UIView leading constraint to Superview (Cell content) for 10px and UILabels leading Constraints for 10 px to the next view (UIView). Later in my code
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(IndexPath *)indexPath {
// ...
Record *record = [self.records objectAtIndex:indexPath.row];
if ([record.imageURL is equalToString:#""]) {
cell.cdView.hidden = YES;
}
}
I am hiding my cell.cdView and I would like the labels to move to the left however they are staying in the same position in Cell. I tried to remove cell.cdView from superview but it didn't work either. I have attached image to clarify what I am about.
I know how to do this programatically and I am not looking for that solution. What I want is to set constraints in IB and I expect that my subviews will move dynamically if other views are removed or hidden. Is it possible to do this in IB with auto-layout?
.....
It is possible, but you'll have to do a little extra work. There are a couple conceptual things to get out of the way first:
Hidden views, even though they don't draw, still participate in Auto Layout and usually retain their frames, leaving other related views in their places.
When removing a view from its superview, all related constraints are also removed from that view hierarchy.
In your case, this likely means:
If you set your left view to be hidden, the labels stay in place, since that left view is still taking up space (even though it's not visible).
If you remove your left view, your labels will probably be left ambiguously constrained, since you no longer have constraints for your labels' left edges.
What you need to do is judiciously over-constrain your labels. Leave your existing constraints (10pts space to the other view) alone, but add another constraint: make your labels' left edges 10pts away from their superview's left edge with a non-required priority (the default high priority will probably work well).
Then, when you want them to move left, remove the left view altogether. The mandatory 10pt constraint to the left view will disappear along with the view it relates to, and you'll be left with just a high-priority constraint that the labels be 10pts away from their superview. On the next layout pass, this should cause them to expand left until they fill the width of the superview but for your spacing around the edges.
One important caveat: if you ever want your left view back in the picture, not only do you have to add it back into the view hierarchy, but you also have to reestablish all its constraints at the same time. This means you need a way to put your 10pt spacing constraint between the view and its labels back whenever that view is shown again.
Adding or removing constraints during runtime is a heavyweight operation that can affect performance. However, there is a simpler alternative.
For the view you wish to hide, set up a width constraint. Constrain the other views with a leading horizontal gap to that view.
To hide, update the .constant of the width constraint to 0.f. The other views will automatically move left to assume position.
See my other answer here for more details:
How to change label constraints during runtime?
For those who support iOS 8+ only, there is a new boolean property active. It will help to enable only needed constraints dynamically
P.S. Constraint outlet must be strong, not weak
Example:
#IBOutlet weak var optionalView: UIView!
#IBOutlet var viewIsVisibleConstraint: NSLayoutConstraint!
#IBOutlet var viewIsHiddenConstraint: NSLayoutConstraint!
func showView() {
optionalView.isHidden = false
viewIsVisibleConstraint.isActive = true
viewIsHiddenConstraint.isActive = false
}
func hideView() {
optionalView.isHidden = true
viewIsVisibleConstraint.isActive = false
viewIsHiddenConstraint.isActive = true
}
Also to fix an error in storyboard you'll need to uncheck Installed checkbox for one of these constraints.
UIStackView (iOS 9+)
One more option is to wrap your views in UIStackView. Once view is hidden UIStackView will update layout automatically
UIStackView repositions its views automatically when the hidden property is changed on any of its subviews (iOS 9+).
UIView.animateWithDuration(1.0) { () -> Void in
self.mySubview.hidden = !self.mySubview.hidden
}
Jump to 11:48 in this WWDC video for a demo:
Mysteries of Auto Layout, Part 1
My project uses a custom #IBDesignable subclass of UILabel (to ensure consistency in colour, font, insets etc.) and I have implemented something like the following:
override func intrinsicContentSize() -> CGSize {
if hidden {
return CGSizeZero
} else {
return super.intrinsicContentSize()
}
}
This allows the label subclass to take part in Auto Layout, but take no space when hidden.
For the Googlers: building on Max's answer, to solve the padding issue that many have noticed I simply increased the height of the label and used that height as the separator instead of actual padding. This idea could be expanded for any scenario with containing views.
Here's a simple example:
In this case, I map the height of the Author label to an appropriate IBOutlet:
#property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;
and when I set the height of the constraint to 0.0f, we preserve the "padding", because the Play button's height allows for it.
connect constraint between uiview and labels as IBOutlet and set priority member to a less value when set hidden = YES
What I ended up doing was creating 2 xibs. One with the left view and one without it. I registered both in the controller and then decided which to use during cellForRowAtIndexPath.
They use the same UITableViewCell class. The downside is that there is some duplication of the content between the xibs, but these cells are pretty basic. The upside is that I don't have a bunch of code to manually manage removing view, updating constraints, etc.
In general, this is probably a better solution since they are technically different layouts and therefore should have different xibs.
[self.table registerNib:[UINib nibWithNibName:#"TrackCell" bundle:nil] forCellReuseIdentifier:#"TrackCell"];
[self.table registerNib:[UINib nibWithNibName:#"TrackCellNoImage" bundle:nil] forCellReuseIdentifier:#"TrackCellNoImage"];
TrackCell *cell = [tableView dequeueReusableCellWithIdentifier:(appDelegate.showImages ? #"TrackCell" : #"TrackCellNoImage") forIndexPath:indexPath];
In this case, I map the height of the Author label to an appropriate IBOutlet:
#property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;
and when I set the height of the constraint to 0.0f, we preserve the "padding", because the Play button's height allows for it.
cell.authorLabelHeight.constant = 0;
Use two UIStackView Horizontal and Vertical, when some subview view in stack is hidden other stack subviews will be moved, use Distribution -> Fill Proporionally for Vertical stack with two UILabels and need set width and height constaints for first UIView
Just use UIStackView and everything will be work fine.
No need to worry about other constraint, UIStackView will handle the space automatically.
For this specific layout the constraint to be working with is the 'leading' constraint on the view that is being hidden. The below theory will work in all directions though.
1: Setup all your constraints how you want it to look when all views are visible.
2: Add a second 'leading' constraint to the view you want to hide. This will break the constraints for a moment.
3: Change the priority of the original leading constraint to be '999' - this then gives priority to your new constraint which will be at 1000 and no constraints will be broken anymore.
4: Change the new constraint from 'leading=leading' to be 'trailing=leading'. This will move the view you want to hide off the leading edge of its parent shifting it out of the way.
5: Toggling the new constraint's isActive value will now toggle if it's in the view or outside it. Set that to true/false at the same time as setting the visibility to true/false. Eg:
#IBOutlet var avatar:UIImage!
#IBOutlet var avatarLeadHid:NSLayoutConstraint!
func hideAvatar() {
self.avatar.isHidden = true
self.avatarLeadHid.isActive = true
}
func showAvatar() {
self.avatar.isHidden = false
self.avatarLeadHid.isActive = false
}
Bonus: You can adjust the 'constant' value of the new hider-constraint in order to alter the padding/margin to use when the view is hidden. This value can be negative.
Extra Bonus: It's possible to see what your layout will look like from within the Interface Builder without running any code just by toggling the 'Installed' checkbox on the hider-constraint.
Further Help: I made a video that shows what I do better that a list of points: https://youtu.be/3tGEwqtQ-iU
In my case I set the constant of the height constraint to 0.0f and also set the hidden property to YES.
To show the view (with the subviews) again I did the opposite: I set the height constant to a non-zero value and set the hidden property to NO.
Try this,I have implemented below code ,
I have one View on ViewController in that added other three views, When any view is hidden other two view will move,Follow below steps.
,
1.ViewController.h File
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (strong, nonatomic) IBOutlet UIView *viewOne;
#property (strong, nonatomic) IBOutlet UIView *viewTwo;
#property (strong, nonatomic) IBOutlet UIView *viewThree;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewOneWidth;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewTwoWidth;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewThreeWidth;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewBottomWidth;
#end
2.ViewController.m
#import "ViewController.h"
#interface ViewController ()
{
CGFloat viewOneWidthConstant;
CGFloat viewTwoWidthConstant;
CGFloat viewThreeWidthConstant;
CGFloat viewBottomWidthConstant;
}
#end
#implementation ViewController
#synthesize viewOne, viewTwo, viewThree;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a
nib.
/*
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1
*/
// [viewOne setHidden:NO];
// [viewTwo setHidden:NO];
// [viewThree setHidden:NO];
// [viewOne setHidden:NO];
// [viewTwo setHidden:NO];
// [viewThree setHidden:YES];
// [viewOne setHidden:NO];
// [viewTwo setHidden:YES];
// [viewThree setHidden:NO];
// [viewOne setHidden:NO];
// [viewTwo setHidden:YES];
// [viewThree setHidden:YES];
// [viewOne setHidden:YES];
// [viewTwo setHidden:NO];
// [viewThree setHidden:NO];
// [viewOne setHidden:YES];
// [viewTwo setHidden:NO];
// [viewThree setHidden:YES];
// [viewOne setHidden:YES];
// [viewTwo setHidden:YES];
// [viewThree setHidden:NO];
// [viewOne setHidden:YES];
// [viewTwo setHidden:YES];
// [viewThree setHidden:YES];
[self hideShowBottomBar];
}
- (void)hideShowBottomBar
{
BOOL isOne = !viewOne.isHidden;
BOOL isTwo = !viewTwo.isHidden;
BOOL isThree = !viewThree.isHidden;
viewOneWidthConstant = _viewOneWidth.constant;
viewTwoWidthConstant = _viewTwoWidth.constant;
viewThreeWidthConstant = _viewThreeWidth.constant;
viewBottomWidthConstant = _viewBottomWidth.constant;
if (isOne && isTwo && isThree) {
// 0 0 0
_viewOneWidth.constant = viewBottomWidthConstant / 3;
_viewTwoWidth.constant = viewBottomWidthConstant / 3;
_viewThreeWidth.constant = viewBottomWidthConstant / 3;
}
else if (isOne && isTwo && !isThree) {
// 0 0 1
_viewOneWidth.constant = viewBottomWidthConstant / 2;
_viewTwoWidth.constant = viewBottomWidthConstant / 2;
_viewThreeWidth.constant = 0;
}
else if (isOne && !isTwo && isThree) {
// 0 1 0
_viewOneWidth.constant = viewBottomWidthConstant / 2;
_viewTwoWidth.constant = 0;
_viewThreeWidth.constant = viewBottomWidthConstant / 2;
}
else if (isOne && !isTwo && !isThree) {
// 0 1 1
_viewOneWidth.constant = viewBottomWidthConstant;
_viewTwoWidth.constant = 0;
_viewThreeWidth.constant = 0;
}
else if (!isOne && isTwo && isThree) {
// 1 0 0
_viewOneWidth.constant = 0;
_viewTwoWidth.constant = viewBottomWidthConstant / 2;
_viewThreeWidth.constant = viewBottomWidthConstant / 2;
}
else if (!isOne && isTwo && !isThree) {
// 1 0 1
_viewOneWidth.constant = 0;
_viewTwoWidth.constant = viewBottomWidthConstant;
_viewThreeWidth.constant = 0;
}
else if (!isOne && !isTwo && isThree) {
// 1 1 0
_viewOneWidth.constant = 0;
_viewTwoWidth.constant = 0;
_viewThreeWidth.constant = viewBottomWidthConstant;
}
else if (isOne && isTwo && isThree) {
// 1 1 1
_viewOneWidth.constant = 0;
_viewTwoWidth.constant = 0;
_viewThreeWidth.constant = 0;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Hope So this logic will help some one.
I will use horizontal stackview. It can remove the frame when the subview is hidden.
In image below, the red view is the actual container for your content and has 10pt trailing space to orange superview (ShowHideView), then just connect ShowHideView to IBOutlet and show/hide/remove it programatically.
This is when the view is visible/installed.
This is when the view is hidden/not-installed.
This my another solution using priority constraint. The idea is set the width to 0.
create container view (orange) and set width.
create content view (red) and set trailing space 10pt to superview (orange). Notice trailing space constraints, there are 2 trailing constraint with different priority. Low(=10) and High(<=10). This is important to avoid ambiguity.
Set orange view's width to 0 to hide the view.
The easiest solution is to use UIStackView (horizontal). Add to stack view: first view and second view with labels.
Then set isHidden property of first view to false.
All constrains will be calculated and updates automatically.
Instead of hiding view, create the width constrain and change it to 0 in code when you want to hide the UIView.
It may be the simplest way to do so. Also, it will preserve the view and you don't need to recreate it if you want to show it again (ideal to use inside table cells). To change the constant value you need to create a constant reference outlet (the same way as you do outlets for the view).
As no_scene suggested, you can definitely do this by changing the priority of the constraint at runtime. This was much easier for me because I had more than one blocking view which would have to be removed.
Here's a snippet using ReactiveCocoa:
RACSignal* isViewOneHiddenSignal = RACObserve(self.viewModel, isViewOneHidden);
RACSignal* isViewTwoHiddenSignal = RACObserve(self.viewModel, isViewTwoHidden);
RACSignal* isViewThreeHiddenSignal = RACObserve(self.viewModel, isViewThreeHidden);
RAC(self.viewOne, hidden) = isViewOneHiddenSignal;
RAC(self.viewTwo, hidden) = isViewTwoHiddenSignal;
RAC(self.viewThree, hidden) = isViewThreeHiddenSignal;
RAC(self.viewFourBottomConstraint, priority) = [[[[RACSignal
combineLatest:#[isViewOneHiddenSignal,
isViewTwoHiddenSignal,
isViewThreeHiddenSignal]]
and]
distinctUntilChanged]
map:^id(NSNumber* allAreHidden) {
return [allAreHidden boolValue] ? #(780) : #(UILayoutPriorityDefaultHigh);
}];
RACSignal* updateFramesSignal = [RACObserve(self.viewFourBottomConstraint, priority) distinctUntilChanged];
[updateFramesSignal
subscribeNext:^(id x) {
#strongify(self);
[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:0.3 animations:^{
[self.view layoutIfNeeded];
}];
}];
In case this helps someone, I built a helper class for using visual format constraints. I'm using it in my current app.
AutolayoutHelper
It might be a bit tailored to my needs, but you might find it useful or you might want to modify it and create your own helper.
I have to thank Tim for his answer above, this answer about UIScrollView and also this tutorial.
Here's how I would re-align my uiviews to get your solution:
Drag drop one UIImageView and place it to the left.
Drag drop one UIView and place it to the right of UIImageView.
Drag drop two UILabels inside that UIView whose leading and trailing constraints are zero.
Set the leading constraint of UIView containing 2 labels to superview instead of UIImagView.
IF UIImageView is hidden, set the leading constraint constant to 10 px to superview. ELSE, set the leading constraint constant to 10 px + UIImageView.width + 10 px.
I created a thumb rule of my own. Whenever you have to hide / show any uiview whose constraints might be affected, add all the affected / dependent subviews inside a uiview and update its leading / trailing / top / bottom constraint constant programmatically.
This is an old question but still I hope it will helps. Coming from Android, in this platform you have an handy method isVisible to hide it from the view but also not have the frame considered when the autolayout draw the view.
using extension and "extend" uiview you could do a similar function in ios (not sure why it is not in UIKit already) here an implementation in swift 3:
func isVisible(_ isVisible: Bool) {
self.isHidden = !isVisible
self.translatesAutoresizingMaskIntoConstraints = isVisible
if isVisible { //if visible we remove the hight constraint
if let constraint = (self.constraints.filter{$0.firstAttribute == .height}.first){
self.removeConstraint(constraint)
}
} else { //if not visible we add a constraint to force the view to have a hight set to 0
let height = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal , toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: 0)
self.addConstraint(height)
}
self.layoutIfNeeded()
}
the proper way to do it is to disable constraints with isActive = false. note however that deactivating a constraint removes and releases it, so you have to have strong outlets for them.
I think this is the most simple answer. Please verify that it works:
StackFullView.layer.isHidden = true
Task_TopSpaceSections.constant = 0. //your constraint of top view
check here https://www.youtube.com/watch?v=EBulMWMoFuw

Resources