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
Related
I have a KSSection UIView subclass that I'm trying to use to do collapsing / expanding of different sections. It has a child view (set by an IBOutlet) called content. The content's size is determined by a number of child views (UILabel, UIImageView, etc.) that are all variable size.
Currently I'm pinning the leading and trailing space of the content to the parent KSSection, aligning it centred vertically, and adding a remove at runtime constraint that the heights of content and section are equal. If I disable the remove at runtime everything works great - except that I can't collapse the view.
How can I calculate the size of the content to be used as the intrinsicContentSize of the KSection? So far I have the following snippet, but the call to intrinsicContentSize always returns UIViewNoIntrinsicMetric for both properties.
#implementation SKContainer
- (CGSize)intrinsicContentSize
{
if (self.collapsed) return CGSizeZero;
else return [self.content intrinsicContentSize];
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self invalidateIntrinsicContentSize];
}
- (void)setCollapsed:(BOOL)collapsed
{
if (_collapsed != collapsed)
{
_collapsed = collapsed;
[self invalidateIntrinsicContentSize];
}
}
#end
Edit:
Sorry to clarify it is actually returning UIViewNoIntrinsicMetric for both dimensions of the CGSize.
Attaching a sample: http://cl.ly/2F3s3X3y2U1H
Okay, so my previous answer was on an unnecessary track. What I ended up doing was this:
First, I removed the section view entirely, leaving us with just the plain vanilla content view to play the role of the section. (The section view was just adding an extra layer of complication.) Then I lowered the priority of the section view height to 250, and ran the project. Presto! The section view now expands, all by itself, driven by the constraints of the labels within it.
Second, here's how I collapse and expand. I keep an outlet to two of the constraints: the section height constraint, and the last constraint in the height stack of the internal constraints. Then my expand/collapse code looks like this:
- (IBAction)toggleButtonSelector:(id)sender
{
self.collapsed = !self.collapsed;
if (self.collapsed) {
self.sectionHeightConstraint.constant = 10; // or whatever height you like
self.sectionHeightConstraint.priority = 999;
[NSLayoutConstraint deactivateConstraints:#[self.bottomInternalConstraint]];
} else {
self.sectionHeightConstraint.priority = 250;
[NSLayoutConstraint activateConstraints:#[self.bottomInternalConstraint]];
}
}
You see, we need to overcome the desire of the internal stack of constraints to keep us expanded, so we remove one of the constraints in order to collapse, and we set the height constraint to a small number and raise its priority. To expand, we reverse that: we restore the missing internal constraint, and lower the priority of the overall section height constraint once again.
EDIT A really cool byproduct of this implementation is that we can now animate the collapse/expand effect merely by appending these lines of code at the end of that method:
[UIView animateWithDuration:1 animations:^{
[self.view layoutIfNeeded];
}];
You are misusing -intrinsicContentSize, both in why you're calling it and in your attempt to implement it for your view class. That method is for returning a size which is "intrinsic" to its nature and contents. It can not depend on other views, other constraints, etc. It also has nothing to do with the view's current size, because a view can be compressed or stretched from its intrinsic size by other constraints.
You should use constraints to make your view depend on its collapsed state and its subview's size (resulting from other constraints in combination with descendant views' intrinsic sizes, if they have any).
For example, assuming you want your view to collapse in the vertical direction, you might have constraints which always pin the content subview to the top, leading, and trailing edges. If your view is collapsed, you would have a constraint to make its height zero. You would not constrain your view's bottom to the content view's bottom. The content view would have its normal height, but that would all be clipped out by virtue of the fact that its superview has zero height.
On the other hand, if your view is not collapsed, you would remove the height constraint on your view and add a constraint connecting your view's bottom to the content view's bottom.
Using solutions from #ken and #matt my final code (still using the SKContainer) is:
#interface SKContainer ()
#property (nonatomic, strong) IBOutlet NSLayoutConstraint *expandedLayoutConstraint;
#property (nonatomic, strong) IBOutlet NSLayoutConstraint *collapsedLayoutConstraint;
#end
#implementation SKContainer
- (void)setCollapsed:(BOOL)collapsed
{
if (_collapsed != collapsed)
{
_collapsed = collapsed;
[self removeConstraint:collapsed ? self.expandedLayoutConstraint : self.collapsedLayoutConstraint];
[self addConstraint:collapsed ? self.collapsedLayoutConstraint : self.expandedLayoutConstraint];
}
}
#end
Where expandedLayoutConstraint is a equal height constraint to the content view and collapsedLayoutConstraint is a height 0 priority 200 constraint.
OK I have a view-controller that sits in a container of another view.
MainView
View
Someview
ContainerView (contains ContainedViewController)
Otherview
ContainedViewController
ContainedView (height can get resized during run).
UILabel (varying height)
UIView (fixed size, width & height)
Here's the thing... the view in ContainedViewController needs to get resized at runtime. It contains a label (that can grow depending on the text in it) and a static view directly below it, that never changes size
So, I have a constraint on the UILabel for it's height, and I change that at runtime, depending on how big it needs to be. There's a vertical constraint between the label and the fixed-view, and all of the "standard" constraints to the main superview.
When I run, though, I get "unable to simultaneously satisfy constraints", followed by the constraint log. Among the conflicting constraints are my new UILabel height, and then the height of ContainedView. I get a "UIView-Encapsulated-View-Height" problem with the ContainedView.
Apparently, "ContainedView"'s height is being dictated by the height of MainView's ContainerView.
What I want is for the ContainedView's height to change when the UILabel's height changes, and then have the propagate back up the containers, all the way to MainView. But I can't seem to get this to work.
How can I get the superview, and it's container view to resize when I change the size of my label?
In your main view controller, declare the following override:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
// Layout the container's content and get its resulting size
[self.containedViewController.view layoutIfNeeded];
CGSize containedViewSize = [self.containedViewController contentSize];
// Now, use containedViewSize to set constraints on the view controller.
self.containerHeightConstraint.constant = containedViewSize.height;
self.containerWidthConstraint.constant = containedViewHeight.width;
}
Then, in your contained view controller, declare these properties:
#property (nonatomic, weak) IBOutlet UIView * contentView;
#property (nonatomic, weak) UIViewController * mainViewController;
In your storyboard make contentView a subview of the contained view controller's main view. Pin it with constraints to the main view's top left corner. Put whatever content you want in contentView, including constraints that determine contentView's size. (DON'T pin the contentView's length and width to the main view, however; that will cause conflicting constraints you described above.)
mainViewController should be set when preparing for the embed segue that sets up the container view controller.
And this method:
- (CGSize)contentSize {
return self.contentView.frame.size;
}
Now, whenever you do something in the container view controller that affects the size of contentView, make the following call:
[self.mainViewController.view setNeedsLayout];
Sorry this isn't simpler, but it's the best solution I've found to this problem so far.
I have created this simple custom UIView
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
internalView = [UIView new];
[self addSubview:internalView];
internalView.backgroundColor = [UIColor whiteColor];
sublabel = [UILabel new];
sublabel.text = #"test";
sublabel.backgroundColor = [[UIColor yellowColor] colorWithAlphaComponent:0.4];
sublabel.textAlignment = NSTextAlignmentCenter;
sublabel.translatesAutoresizingMaskIntoConstraints = NO;
[internalView addSubview:sublabel];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[internalView setFrame:self.bounds];
[sublabel setFrame:internalView.bounds];
}
#end
The view is used in a xib where are defined 4 constraints:
The constrains are related to the top, trailing, leading and the height.
The views hierarchy of the custom view is self -> internalView -> sublabel
The layoutSubviews is doing a simple thing like setting the frame of each subview with the bounds of the superview.
I created the code above just to point out a strange behaviour i found in translatesAutoresizingMaskIntoConstraints.
(The yellow view is the label)
If the value of the property is YES the results is what I expect from the code in the layoutSubviews method
If it is NO, with the same layoutSubviews i got:
The documentation of the translatesAutoresizingMaskIntoConstraints says:
If this is value is YES, the view’s superview looks at the view’s
autoresizing mask, produces constraints that implement it, and adds
those constraints to itself (the superview).
In both cases the autoresizingMask is set to UIViewAutoresizingNone and there are no constraints in the constraints property of the label.
Why with the translatesAutoresizingMaskIntoConstraints=NO the frame that i set in the layoutSubviews is not what i see on screen?
Edit 2:
I expect, given the exposed scenario, to have the same results with translatesAutoresizingMaskIntoConstraints set to YES or NO.
Edit:
I tried to swizzle the sizeToFit method to check if it is called. It's not.
This is happen in iOS6/7
Update 08/08/14
After further investigation i discovered that there is a way to change the frame of the label without having autolayout playing around.
I discovered that after the first pass of layout (when is called the layoutSubviews) with the translatesAutoresizingMaskIntoConstraints=NO autolayout adds constraints for the hugging/compression of the UILabel. The point is that for every view that implements intrinsicContentSize returning something different from UIViewNoIntrinsicMetric the autolayout adds specific constrains. That is the reason behind the resizing of the label.
So the first thing that i did is to reimplement a subclass of the UILabel to override the
intrinsicContentSize.
After that, following the suggestions in this really good article http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html, I tried to turn of autolayout completely for the subviews involved removing [super layoutSubviews].
The goal for me was to avoid that autolayout could act on views where a was trying to apply animated transformations. So if you have the same needs i hope this can help you.
This comes more from intuition of having used it than actual study, but...
If you set translatesAutoresizingMaskIntoConstraints to YES, the system will create constraints to enforce the frame you defined for your view.
If it is set to NO, no constraints will be set for the view, and as you did not set them yourself either, the view will be resized according to default behaviours. In this case it seems to resize to the minimum content size because of the "contentHugging" values.
Bottom line is, from my understanding, when auto-layout is active all views need constraints to be properly placed. You either set that property to YES, or set the constraints yourself. If you don't do either, results will be a bit unpredictable.
The UIViewAutoresizingNone mask is still a valid mask, with fixed dimensions (no resizing) and it will be translated to constraints. Views can coexist without setting constraints, when you set that option to YES.
You are interpreting UIViewAutoresizingNone as meaning "no mask" when it really means "mask with no resizing". Sorry to disagree with you, but I think that this is the expected behaviour :)
I am working on my first project using Auto Layout and custom views. My question is this:
I created my custom view in Interface Builder then added constraints to stretch the view if needed which is working the way I want it to, however, consider the following code snippet from my custom view class -
// MyCustomView
-(id)initWithCoder:(NSCoder*)decoder
{
self = [super initWithCoder:decoder];
if(self != nil)
{
CGFloat layerWidth = self.bounds.size.width;
CGFloat layerHeight = self.bounds.size.height;
}
return self;
}
This code return the size set in IB. My drawing code relies on the new width and height (if the view has been stretched) but I don't know how to retrieve them.
First, that code is utterly silly because you are creating variables layerWidth and layerHeight and throwing them away, which is pointless.
Second, self.bounds.size is always the view's width and height. However, it is pointless to ask about this in initWithCoder:, which (as you have rightly seen) happens long before the view is put into the interface and even longer before the auto layout takes place that resizes it. If your drawing code relies on the bounds size, then retrieve the bounds size when you draw. If you need to draw again because the view has changed size, and if this is not happening all by itself, then implement layoutSubviews to tell the view that it needs to be drawn again.
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.