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.
Related
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
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.
I have a screen layout where there are two resizable labels , which will contain multiline text. These labels are placed inside their parent views which intern are added to main contentView, main contentView is then added to scrollView ( thats what most of the solutions suggests). For both the labels (below About and Time and location labels in first image attached) I have set height constraints as "greater than or equal to" and setting the numberOfLines to 0 as well as calling SizetoFit, but actual output is not as expected (see second image attached). There are no constraints warnings. All constraints are provided for all the elements.
The code in viewDidLoad is as follows for one of the label.
self.lblAbout.text = #"this is a long two three lines about string which will have two lines this is a long two three lines about string which will have two lines";
self.lblAbout.numberOfLines = 0;
[self.lblAbout sizeToFit];
[self.lblAbout setPreferredMaxLayoutWidth:244.0];
Also
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
scrollView.contentSize = contentView.frame.size;
}
Not if any additional constraints are needed, I have added all leading , trailing , top , bottom constrains along with height wherever needed, plus the spacing between all the views is in place.
What i want is the labels should get adjusted to number of lines and the contentView (parent view) should scroll inside ScrollView as the total height will be larger than the screen available.
*** problem I think is the outer view of the labels aren't getting resized as per the label because of which all the views below it aren't getting repositioned ****
Please try this Solution,
1. add Height Constraint to superView of your Label.
2. add IBOutlet of that Constraint
3. add this Method to find out Height of your Text
you need to give width of super view of your Label so width will be same as your super view
4. Now get Height form returned CGRect and assign it to your Constraints's constant. it should be like
heightConstraint.constant = youObject.size.height;
please Make sure you have added other Constraints accordingly this. if not than you need to also increase Height of other superviews accordingly.
(CGRect)sizeOfDetailLabelFromString:(NSString*)string maxWidth:(CGFloat)maxWidth{
NSDictionary *attributes = #{NSFontAttributeName:FONT_LIGHT};
CGRect rect = [string boundingRectWithSize:CGSizeMake(maxWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributes context:nil];
return rect;
}
Here is what worked finally.
Added ScrollView in main View
Added View as a contentView ( be sure to rename it in designer to something than just view )
pinned scrollview to main view using leading trailing top and bottom space constraints.
Pinned contentView to scrollview same as above
Added all the components with their respective constraints
Set the height of UILabel which i want to resize using "Greater than or equal to constraint (this is necessary) and set number of lines to 0 in code
The parent view of label shouldn't have any fixed height but enough constraints to calculate it at runtime.
Make sure ScrollView has no ambiguity in calculating contentSize.
imp - Add constraints to width of Main view , scrollview , contentView ( that was in my case , you may not need equal width constraint between contentView and scrollview , but between contentView and main view its necessary)
Now scrollview scrolls exactly the way it's needed.
To my belief most of the above things i already did but somehow it wasn't working ,deleted everything and did all the things again few times and it worked.
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 am struggling with maybe a bit of a rookie issue. I have a UIView within which I display some price. I want the UIView to be of a dynamic width according to the price, if its 1 Euro, then it will be e.g. 20pt, if its 2300 Euro, then it will be like 50pt in width.
I was trying to use the storyboard's constraints but without luck. Is it possible to do it within storyboard or do I have to calculate the width of UILabel and then set the width of UIView programmatically?
Thank you in advance.
Yes, you can do this in the storyboard. Add a label to your view and pin it to the left and right edge (top and bottom if you want also). Give the view constraints to its superview in the x and y directions, but do not give it a width constraint (it will need a height constraint if you didn't pin the top and bottom of the label to it). The view should then expand with the label depending on its content.
In general, auto layout is performed in a top-down fashion. In other words, a parent view layout is performed first, and then any child view layouts are performed. So asking the system to size the parent based on the child is a bit like swimming upstream, harder to do, but still possible with some work.
One solution is to use the intrinsic size of a view.
For example, a UILabel has an intrinsic size based on the text in the label. If a UILabel has a leading constraint and a top constraint, but no other constraints, then its width and height are determined by its intrinsic size.
You can do the same thing with a custom view class that encloses a UILabel. By setting the intrinsic size of the custom view class based on the intrinsic size of the UILabel, you get a view that automatically resizes based on the text in the label.
Here's what the code looks like for the custom class. The .h file defines a single property text. The .m file has an IBOutlet to the child label. Setting and getting the text property simply sets or gets the text from the label. But there's one very important twist, setting the text invalidates the intrinsic size of the parent. That's what makes the system adjust the size of the parent view. In the sample code below the parent is sized to have an 8 pixel margin all around the UILabel.
SurroundView.h
#interface SurroundView : UIView
#property (strong, nonatomic) NSString *text;
#end
SurroundView.m
#interface SurroundView()
#property (weak, nonatomic) IBOutlet UILabel *childLabel;
#end
#implementation SurroundView
- (void)setText:(NSString *)text
{
self.childLabel.text = text;
[self invalidateIntrinsicContentSize];
}
- (NSString *)text
{
return( self.childLabel.text );
}
- (CGSize)intrinsicContentSize
{
CGSize size = self.childLabel.intrinsicContentSize;
size.height += 16;
size.width += 16;
return( size );
}
#end
Creating the IBOutlet to the childLabel can be a little tricky, so here's the procedure
drag out a UIView into the storyboard
use the Identity inspector to change the class to SurroundView
drag out a UILabel and add it as a subview of the SurroundView
select the label, and open the assistant editor
show SurroundView.m in the assistant
drag from the open circle to the label as shown below
All that's left is to get the constraints right. The constraints for the label should look like this
The constraints for the SurroundView should be as shown below. The key point is that the Intrinsic Size should be set to Placeholder to avoid the warnings about missing constraints.
Place the label inside the view and pin its TOP , BOTTOM , TRAILING and LEADING edges to the labels superview. Note that you do not specify the width constraint. Now add a height and width constraint to the view. Make an outlet to the width constraint and when the price changes set the view's width constraint's constant to your desired value. Since the label is pinned to the view it will expand too.