IOS: Adjust UIButton height depend on title text using Autolayout? - ios

I have a UIButton and it can change the title at the runtime. Therefore, I want to increase the UIButton height depend on the title text for display full text by using AutoLayout.
I can increase the UILabel height by set the height constraint to "Greater than or Equal" but it not work with UIButton.
I have used [myButton sizeToFit] but it only increase the UIButon width (not increase height).
My current UIButton properties now is
- constraint height: 30
- leading : 15
- trailing: 15
- top: 5
- fontsize: 12
UPDATE
I created an IBOutlet for constraint height of UIButton for changing the height as #NSNood said.
Then I need to use \n in title text to split line.
But I don't know where should I put the \n?
Here is the Button that I want in portrait
Here is the Button that I want in landscape
How can I determine the place to put \n?
Please guide me how to achieve it with AutoLayout. Any help would be appreciated.

Sorry that I didn't follow the post, lately and thus am coming up with a real late solution. Still I'm writing the answer as a reference, if someone might find it useful in future.
First of all let's show the storyboard configuration for the button. Those are depicted in the following pictures:
The picture shows that I have added only left, top and right constraints for the button and nothing else. This allows the button to have some intrinsicContentSize for it's height but it's width is still determined by it's left and right constraints.
The next phase is to write some ViewController class that shall contain the button. In my VC, I have created an outlet for the button by name button:
#property(nonatomic,weak) IBOutlet UIButton* button;
and has attached it to the storyboard button. Now I have overridden two methods, namely, viewDidLoad and viewWillLayoutSubviews like below:
-(void)viewDidLoad {
[super viewDidLoad];
self.button.titleLabel.numberOfLines = 0;
self.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.button setTitle:#"Chapter One\n "
"A Stop on the Salt Route\n "
"1000 B.C.\n "
"As they rounded a bend in the path that ran beside the river, Lara recognized the silhouette of a fig tree atop a nearby hill. The weather was hot and the days were long. The fig tree was in full leaf, but not yet bearing fruit." forState:UIControlStateNormal];
}
The viewDidLoad method ensures the titleLabel (the label that
holds button text) is multiline and if some large text comes to it,
it wraps the text by wrapping words.
The viewWillLayoutSubviews method ensures button layouting process
occurs whenever bounds of the main view change, e.g. due to the
change of interface orientation.
The final and the most effective part is to manually handle the layout process for the button. For this purpose, we need to subclass UIButton. I have written a subclass named MyButton that inherits from UIButton and you might use whatever name you like. Set this as the custom class for the button in Identity Inspector.
The subclass overrides two methods, namely, intrinsicContentSize and layoutSubviews. The class body looks something like the following:
#import "MyButton.h"
#implementation MyButton
-(CGSize)intrinsicContentSize {
return [self.titleLabel sizeThatFits:CGSizeMake(self.titleLabel.preferredMaxLayoutWidth, CGFLOAT_MAX)];;
}
-(void)layoutSubviews {
self.titleLabel.preferredMaxLayoutWidth = self.frame.size.width;
[super layoutSubviews];
}
#end
The UIButon subclass takes the ownership of the layout process by overriding layoutSubviews method. The basic idea here is to determine the button width, once it has been layout. Then setting the width as preferredMaxLayoutWidth (the maximum width for layouting engine, that a multiline label should occupy) of it's child titleLabel (the label that holds button text). Finally, returning an intrinsicContentSize for the button based on it's titleLabel's size, so that the button fully wraps it's titleLabel.
The overridden layoutSubviews is called when the button is already
layed out and it's frame size is determined. At it's first step,
button's rendered width is set as preferredMaxLayoutWidth of the
button's titleLabel.
The second step re-invokes the layouting engine by calling [super
layoutSubviews], so that the buttons intrinsicContentSize is
re-determined based on it's titleLabel's
preferredMaxLayoutWidth, which is set to buttons rendered width,
by now.
In the overridden intrinsicContentSize method we return the
minimum fitting size for the button that fully wraps it's
titleLabel with preferredMaxLayoutWidth set. We use
sizeThatFits fits method on the button's titleLabel and that
simply works as titleLabel doesn't follow any constraint based
layout.
The outcome should be something similar to that you might have required.
Feel free to let me know about any other clarification/concern.
Thanks.

Ayan Sengupta solution in Swift, with support for contentEdgeInsets (thanks Claus Jørgensen):
(You may also further customize the code to take titleEdgeInsets into account if needed)
Subclass your UIButton to take the ownership of the layout process:
/// https://stackoverflow.com/a/50575588/1033581
class AutoLayoutButton: UIButton {
override var intrinsicContentSize: CGSize {
var size = titleLabel!.sizeThatFits(CGSize(width: titleLabel!.preferredMaxLayoutWidth - contentEdgeInsets.left - contentEdgeInsets.right, height: .greatestFiniteMagnitude))
size.height += contentEdgeInsets.left + contentEdgeInsets.right
return size
}
override func layoutSubviews() {
titleLabel?.preferredMaxLayoutWidth = frame.size.width
super.layoutSubviews()
}
}
Use this class in your storyboard, and set constraints for Leading, Trailing, Top, Bottom. But don't set any Height constraint.
An alternative without subclassing is to add a wrapper view as suggested by Bartłomiej Semańczyk answer and Timur Bernikowich comment.

The point is that if you set sizeToFit property, then the text will always be in one line and the width of the button will increase unless you put a next-line sign \n to explicitly say that you want it to be several lines.
You put '\n' in the end of the first line like "line \n line" which represents
line
line
If you want to have two different string values (with \n positioned differently) for Portrait and Landscape you can check the orientation condition using UIDeviceOrientation (UIDevice.currentDevice.orientation) described here and set a string value depending on the orientation of the device

There is a way I always used:
Add another reference UILabel which lineNumber=0 and the same width with the target button.
Do not set height constraint for the ref-UILable, and should set a height constraint for the button to adjust its height
Set the same text to the ref UILabel with the button.titleLable, sizeTofit it and get its frame.size.height
Use the height value to the height constraint of the target button. (Of course, the button.titleLabel linenumber should be set to 0 or more lines)
Done. :)
PS1. This way can be used for the button and ref-label in a scrollview.
PS2. In some case, we can not get the correct height of the ref-label because it cannot gain a correct frame.width in scrollview, especially when we use the trailling constraint. We could consider to define a fixed width to the ref-label before sizeTofit and obtain the correct height for target button use.

Related

Button width and height is disabled in horizontal stackview

Im learning iOS development right now, XCode doesnt allow me to edit width and height of buttons which are in stack view:
In the Storyboard I create a new button of size 30 x 30 with a custom image and then make more 5 copies of that button. Then I embed them after selecting all of them in a Stack View. Now a disaster happens, the buttons are resized to god knows what size and they appear huge and when I try to go to size inspector to resize those buttons I see that "Width" and "Height" fields are disabled.
I tried few suggestions on stackoverflow and selected the stack view and change the distribution of stack view to "Fill Equally" but still the buttons size is being changed. I dont want this to happen. I want a fixed size buttons in a horizontal stack view and putting them in stack view should not change the size or shape of buttons like this. Can anyone please tell me how do I fix this problem?
Please help.
Sometime Interface Builder is not easy to handle because it is a running layout system at design-time / IB_DESIGNABLE. You make changes, IB gets triggered to 'think', changes parameters, layouts again, you see it does not fit and you change again.
It can be easier to fix UIStackView's constrains to your outer layout before dropping content that will be arranged by taking intrinsicContentSize of the subviews into its calculation. Even worse, if the stackview does not have complete constrains already and you drop something in as being arranged, it will take the default size as intrinsicContentSize of the dropped view and change the stackview spacing as it should. This is no surprise but it can be frustrating as convenience is disturbing your workflow here.
The docs tell you should not change intrinsicContentSize because it is not meant to be animated, it will even disturb animations and layout or even break constrains. Well, you can not set intrinsicContentSize, it is read-only. As thats for good reasons they could have written that while UIView's are instanced they can have supportive variables which have to be set before laying out which allows you to make pre-calculations.
While in code this can be tricky also, you can subclass UIView to make arranged subview instances more supportive to your needs.
There is UIView's invalidateIntrinsicContentSize that triggers the layout to take changed intrinsicContentSize into the next layout cycle. You still cant set intrinsicContentSize, but thats not needed when you would have a class designed like shown below.
// IntrinsicView.h
#import UIKit
IB_DESIGNABLE
#interface IntrinsicView : UIView
-(instancetype)initWithFrame:(CGRect)rect;
#property IBInspectable CGFloat intrinsicHeight;
#property IBInspectable CGFloat intrinsicWidth;
#end
// IntrinsicView.m
#import "IntrinsicView.h"
#implementation IntrinsicView {
CGFloat _intrinsicHeight;
CGFloat _intrinsicWidth;
}
- (instancetype)initWithFrame:(CGRect)frame {
_intrinsicHeight = frame.size.height;
_intrinsicWidth = frame.size.width;
if ( !(self = [super initWithFrame:frame]) ) return nil;
// your stuff here..
return self;
}
-(CGSize)intrinsicContentSize {
return CGSizeMake(_intrinsicWidth, _intrinsicHeight);
}
-(void)prepareForInterfaceBuilder {
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, _intrinsicWidth,_intrinsicHeight);
}
#end
Now this gives you control of the behaviour when UIStackView will layout.
Let's look at instancing of your UIStackView.
#import "IntrinsicView.h"
- (void)viewDidLoad {
[super viewDidLoad];
UIStackView *column = [[UIStackView alloc] initWithFrame:self.view.frame];
column.spacing = 2;
column.alignment = UIStackViewAlignmentFill;
column.axis = UILayoutConstraintAxisVertical; //Up-Down
column.distribution = UIStackViewDistributionFillEqually;
CGFloat quadratur = 30.0;
for (int row=0; row<5; row++) {
IntrinsicView *supportiveView = [[IntrinsicView alloc] initWithFrame:CGRectMake(0, 0, quadratur, quadratur)];
// supportiveView stuff here..
[column addArrangedSubview:supportiveView];
}
[self.view addSubview:column];
}
Don't forget IntrinsicView's intrinsicContentSize is set before instancing is complete, so this example takes frame size at initWithFrame as intended and stores that size to be used when intrinsicContentSize is asked. Having that still needs that UIStackView is large enough to layout nicely but you forced the arranged subviews to that intrinsic size. Btw. the example is arranged up..down.
You can use the IntrinsicView in Interface Builder, just change the views inside UIStackView to the above written class. IB will automatically update the designable API and serve you propertys you can set up. This still needs the StackView to have at least width and height set and also constrains if needed. But it takes away the impression your width and height of arranged views would have any effect other than expected, because IntrinicViews height + width is inactive in IB then.
Just to show you how much this improves your possibilities in IB, see image

Auto layout - Replace origin of one UILabel with other

I have two UILabel's. One UILabel is on the top of other. What I want is, if there is no content in top one, the bottom one should take the origin of top.
I have to use Auto Layout on the screen. I tried using sizeToFit but that is not working. Bottom UILabel is still stuck at it's origin if there isn't no content in top UILabel.
Setting constraint for height will not work. The y position of second label will not get its correct Y axis value (It will shift upwards). I solve your problem by playing with vertical spacing constraint between two results.
Explanation:- Set the normal basic constraints for label plus vertical spacing constraint. When there is no text in upper label, then update the vertical spacing constraint as follows:-
verticalSpacingConstraintBetweenLabels.constant = -(KIntialVerticalSpacing + (KHeightOfLabel-KIntialVerticalSpacing));
I create a sample project for you. Source code is available at:-
https://www.dropbox.com/s/eq9hinnw4sdfltq/LabelSpacing.zip?dl=0
One alternative to that requirement is to make IBOutlet for the height of the upper label and set it to 0 whenever you don't want to show it. But keep in mind you have to set vertical spacing between upper and lower label. The lower label shouldn't have top margin to superview.
First drag both top constrains to UIViewController.
Second paste this code:
-(void) loadLabels{
_firstLabel.text=#"";
NSInteger heidFirstLabel = 20;
if(_firstLabel.text.length == 0){
_constrainTopFirstLabel.constant = 0 - heidFirstLabel;
_firstLabel.hidden = YES;
}else{
_constrainTopFirstLabel.constant = 8 ;
}
}
I always use this.
In iOS9 apple has introduces a new class UIStackView to solve this and other problems that we the developers were facing, using auto layout. If you are targeting iOS 9 and later, you should use this class.
If you are targeting versions before iOS 9, one of the many ways could be using intrinsic content size.
1.Subclass UILabel
2.Add a property that determines if this label is collapses or not
#property (nonatomic,assign) BOOL isLabelCollapsed;
3.Override its method intrinsicContentSize
- (CGSize)intrinsicContentSize {
if(self.isLabelCollapsed)
return CGSizeMake(0, 0);
else
return [super intrinsicContentSize];
}
4.Set you collapsable labels class as your custom class.
5.When needed, set your isLabelCollapsed property to YES and call invalidateIntrinsicContentSize on you collapsble label.

Which UILabel method is invoked when I set text, and it resizes itself to fit?

I have an UILabel, created in Universal storyboard, and I have mentioned all required constraints for its position OTHER THAN WIDTH. So it resizes itself as per the text set. Fantastic! Exactly what I want.
Problem starts here : It has background color as green color, but that color is wrapping my text tightly. I thus believe that making it a little wider can help me. But to do that, I need to know which method of my UILabel subclass is invoked. So that I can override and add additional width of 10 points.
BottomLine: Which UILabel method is invoked for resizing the label automatically after I assign it the text?
The way it currently looks :
Unfortunately, we don't have any contentEdgeInsets property we can set on a UILabel (as we do have on a UIButton). If you want auto layout to continue to make the height and width constraints itself, you could make a subclass of UILabel and override the intrinsicContentSize and sizeThatFits to achieve what you want.
So, something like:
- (CGSize) intrinsicContentSize
{
return [self addHorizontalPadding:[super intrinsicContentSize]];
}
- (CGSize)sizeThatFits:(CGSize)size
{
return [self addHorizontalPadding:[super intrinsicContentSize]];
}
- (CGSize)addHorizontalPadding:(CGSize)size
{
return CGSizeMake(size.width + (2*kSomeHorizontalPaddingValue), size.height);
}
Note that this only touches the horizontal padding, but can obviously be modified to add vertical padding as well.
Steffen's answer is the way to go if you want to do that programmatically. I usually have a generic custom label subclass in my projects that adds a (IBInspectable) contentInsets property, amongst other things.
Anyways, just wanted to point out that you can also do this completely in IB by just wrapping your label in another view, give the container view the background color and add constraints for your horizontal padding.

UIButton with increasing height if width is not enough?

I need a button to set up via autolayout which height will increase if the horizontal space is not enough i.e. after rotation. I have a leading and trailing constraint to the parent view, and in portrait mode there is lack of space, so I was thinking about not to reduce font size, but increase height and set Word Wrap for Line Break. I was experimenting size classes, but in somehow always the wCompact hAny is used, because I am testing in iPhone. So to set different height for button for different size classes had no effect.
Anybody has idea how to set depending button height on the available button width and the content via autolayout constraint?
On iPhone button below needs two lines, not 30px, but appr. 60px.
Why are you using a button? This is not what a button is for. If you have that amount of text to display then use either a UILabel or UITextView. You can then add a gesture recogniser to it to capture the tap.
You can use UILabel and can use UITapGestureRecognizer on it. and set number of lines=0 in nib file and have one action of tap gesture. it will work like a button with almost no code as u want
Because I am suspicious it is not possible with Interface Builder I made it with subclassing an UIButton:
class DynamicHeightButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let size = (self.titleForState(UIControlState.Normal)! as NSString).boundingRectWithSize(CGSizeMake(self.bounds.size.width, CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : UIFont.systemFontOfSize(17)], context: nil)
self.bounds.size.height = size.height + 8
}
}

Word Wrap not working for UILabel

Using autolayout I can't override my label in code. I've set the labels attributes in IB: Lines = 0, LineBreaks = Word Wrap, but I have my height set to a single line because due to what cell is selected determines what text goes in the label. So sometimes the label will only have one line.
In my viewDidLoad:
myLabel.text = #”blah, blah, blah….”;
[myLabel setLineBreakMode:NSLineBreakByWordWrapping];
myLabel.numberOfLines = 0; //have tried 1 but didn’t help
[myLabel sizeToFit];
This works on another project, but I wasn’t using AutoLayout. AutoLayout seems to override these settings.
I’ve even added
[myLabel setFrame:CGRectMake(20, 135, 280, 80);
but it doesn’t help.
Allow the intrinsic size of the label determine the height. You are correct that you need to set the numberOfLines property to 0. Since you are using AutoLayout, don't call sizeToFit and you need set the preferredMaxLayoutWidth of the label.
That's because your label's properties are set only once (in viewDidLoad), while the constraints from autolayout are applied everytime your view's layoutSubviews is called.
Also, using a line break mode that wraps the text won't work well with your UILabel if it's adjusting fonts, as per Apple's docs.
If this is a UIViewController, move the UILabel override code into - (void)viewDidLayoutSubviews, or if this is in a UIView, move the code to - (void)layoutSubviews.
Don't forget to call [super viewDidLayoutSubviews] or [super layoutSubviews] in those calls.
That being said, if you see yourself needing to override your constraints, either set up the constraints and properties to what you want in the nib file, otherwise use pure code to set up your label.
You need to set preferredMaxLayoutWidth to the maximum width your label can be. You should do this in viewWillLayoutSubviews.

Resources