UILabel breaks early inside UIStackView - ios

Given the view hierarchy:
UIStackView
--UILabel
--UISwitch
The label breaks too early, even if it can be fit to a single line.
Setting numberOfLines = 1 forces the label to be laid out correctly.
How to make UILabel perform line break only when needed?
Code:
private lazy var title: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
private lazy var toggle = UISwitch()
private lazy var stack = UIStackView(axis: .horizontal,
distribution: .equalSpacing,
alignment: .center,
views: [title,
toggle])
func setupConstraints() {
stack.snp.makeConstraints { (make) in
make.edges.equalTo(contentView.layoutMarginsGuide)
}
}
Result:
Setting numberOfLines = 1 gets me what I'd like to achieve, but the label looses its multi-line functionality:
How to force the desired behavior without losing support for multi-line labels?
When there is a lot of horizontal space, the label gets laid out correctly no matter of the numberOfLines property:

Set your UISwitch's content hugging and resistance priority to 1000.
And stack view distribution and alignment to fill.
Extra Note - If you want label and switch to be top aligned, then set alignment to top.

In your stack view you can give a constraint to your label and switch...
1) give your label leading, top , trailing and bottom constraint. Don't give Width constraint. In trailing constraint take second item Switch.
2) give your switch trailing, top, bottom and Fix width.
Hope it Will work.

Add label inside another stack view.
UIStackView
--UIStackView
--UILabel
--UISwitch

Related

UIScrollView with dynamically sized content

(Xcode 11, Swift)
Being a newbie to iOS and Autolayout, I'm struggling with implementing a fairly simple (IMHO) view which displays a [vertical] list of items. The only problem is that items are decided dynamically and each of them could be either text or image (where either of those could be fairly large so scrolling would be required). WebView is not an option, so it has to be implemented natively.
This is how I understand the process:
Make in IB a UIScrollView and size it to the size of the outer frame.
Make a container view as a subview of UIScrollView (again, in IB) and size it the same.
Set constraint on equal width of both
At runtime, populate container view with UILabels/UIImageViews and also set constraints programmatically to ensure proper layout.
"Tell" scrollview about the subview height in order to make it manage the scrolling thereof.
Is this the right approach? It doesn't seem to work for me (for a toy example of dynamically adding a very tall image to a container view - I cannot get the scrolling to work). What would be the proper way to do the last step in the process above - just force the contentSize of the scrollview to the size of the populated container view (it doesn't seem to work for me). Any help would be appreciated.
When adding multiple elements to a scroll view at run-time, you may find it much easier to use a UIStackView... when setup properly, it will automatically grow in height with each added object.
As a simple example...
1) Start by adding a UIScrollView (I gave it a blue background to make it easier to see). Constrain it to Zero on all 4 sides:
Note that we see the "red circle" indicating missing / conflicting constraints. Ignore that for now.
2) Add a UIView as a "content view" to the scroll view (I gave it a systemYellow background to make it easier to see). Constrain it to Zero on all 4 sides to the Content Layout Guide -- this will (eventually) define the scroll view's content size. Also constrain it equal width and equal height to the Frame Layout Guide:
Important Step: Select the Height constraint, and in the Size Inspector pane select the Placeholder - Remove at build time checkbox. This will satisfy auto-layout in IB during design time, but will allow the height of that view to shrink / grow as necessary.
3) Add a Vertical UIStackView to the "content view". Constrain it to Zero on all 4 sides. Configure its properties to Fill / Fill / 8 (as shown below):
4) Add an #IBOutlet connection to the stack view in your view controller class. Now, at run-time, as you add UI elements to the stack view, all of your "scrollability" will be handled by auto-layout.
Here is an example class:
class DynaScrollViewController: UIViewController {
#IBOutlet var theStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
// local var so we can reuse it
var theLabel = UILabel()
var theImageView = UIImageView()
// create a new label
theLabel = UILabel()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theLabel.translatesAutoresizingMaskIntoConstraints = false
// multi-line
theLabel.numberOfLines = 0
// cyan background to make it easy to see
theLabel.backgroundColor = .cyan
// add 9 lines of text to the label
theLabel.text = (1...9).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .yellow
// add 5 lines of text to the label
theLabel.text = (1...5).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// create a new UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load an image for it - I have one named background
if let img = UIImage(named: "background") {
theImageView.image = img
}
// let's give the image view a 4:3 width:height ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .green
// add 2 lines of text to the label
theLabel.text = (1...2).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load a different image for it - I have one named AquariumBG
if let img = UIImage(named: "AquariumBG") {
theImageView.image = img
}
// let's give this image view a 1:1 width:height ratio
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
}
}
If the steps have been followed, you should get this output:
and, after scrolling to the bottom:
Alignment constraints (leading/trailing/top/bottom)
The alignment constraint between Scroll View and Content View defines the scrollable range of the content. For example,
If scrollView.bottom = contentView.bottom, it means Scroll View is
scrollable to the bottom of Content View.
If scrollView.bottom = contentView.bottom + 100, the scrollable
bottom end of Scroll View will exceed the end of Content View by 100
points.
If scrollView.bottom = contentView.bottom — 100, the bottom of
Content View will not be reached even the scrollView is scrolled to
the bottom end.
That is, the (bottom) anchor on Scroll View indicates the (bottom) edge of the outer frame, i.e., the visible part of Content View; the (bottom) anchor on Content View refers to the edge of the actual content, which will be hidden if not scrolled to.
Unlike normal use cases, alignment constraints between Scroll View and Content View have nothing to do with the actual size of Content View. They affect only “scrollable range of content view” but NOT “actual content size”. The actual size of Content View must be additionally defined.
Size constraints (width/height)
To actually size Content View, we may set the size of Content View to a specific length, like width/height of 500. If the width/height exceeds the width/height of Scroll View, there will be a scrollbar for users to scroll.
However, a more common case will be, we want Content View to have the same width (or height) as Scroll View. In this case, we will have
contentView.width = scrollView.width
The width of Content View refers to the actual full width of content. On the other hand, the width of Scroll View refers to the outer container frame width of Scroll View. Of course, it doesn’t have to be the same width, but can be other forms like a * scrollView.width + b.
And if we have Content View higher (or wider) than Scroll View, a scrollbar appears.
Content View can not only be a single view, but also multiple views, as long as they are appropriately constrained using alignment and size constraints to Scroll View.
For details, you may follow this article: Link.

UIStackView; Equal Spacing Between & Outside Elements

UIStackView is awesome, I love Equal Spacing Distribution.
But how to achieve the same space also outside of elements dynamically?
In my case all elements will have same ratio 1:1
You can add equal spacing using the story board as shown here:
Source: https://stackoverflow.com/a/32862693/3393964
#Declan has the right idea. Here's that answer programatically where you add extra views on either side so the stack view gives correct outside spacing with any number of buttons.
stackView.alignment = .center
stackView.axis = .horizontal
stackView.distribution = .equalCentering
// Then when I add the views...
let leftView = UIView()
stackView.addArrangedSubview(leftView)
content.buttons.forEach { (button) in
stackView.addArrangedSubview(button)
}
let rightView = UIView()
stackView.addArrangedSubview(rightView)
Here's what my view looks like with 2 items using equalSpacing
And here it is with equalCentering distribution, also a nice look.
I prefer to let the UIStackView handle the spacing. Create a UIStackView with equal spacing and add two 0px wide (0px high if using a vertical stackview) transparent views to the the far sides of your stack view.
You can use constraints and give then same height and width. So when you change the dimension of anyone of the component then all components are changed with same dimension.
I think what you want is to have the same spacing outside of the stack view with the spacing outside.
What I would do is the put stack view inside another view (GRAY VIEW) and set the leading and trailing constraint of the stack view to be equal to the spacing of the stack view.
Spacing of the Stack View
Constraints of the Stack View from its super view (Gray View)

Self sizing cells with three UILabels not working

This is the UITableViewCell I have:
The three UILabels have trailing, top, bottom and leading constraints.
This is the hugging priority and compression resistance priority for the UILabel Name:
This is the hugging priority and compression resistance priority for the UILabel Location:
This is the hugging priority and compression resistance priority for the UILabel Type:
On the viewDidLoad from my UITableViewController I'm doing something like this:
self.tableView.estimatedRowHeight = 80
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.reloadData()
But when I run the app, the UITableViewCells don't self sizing:
What am I doing wrong or what do I need to do to make the UITableViewCells make them self sizing?
Edit
I set to 0 the numberOfLines for each UILabel, now the location UILabel doesn't appear
It is most likely that your Text turned out larger in runtime causing the total height in your cell View to turn out greater than the row height itself.
First equate all the hugging priority and then try changing the spacing distance between the Labels with the relation 'Greater than or Equal' and then set the constant to something small like zero. If you still get a constraint error, increase your tableView row height.
Alternative method:
Add all 3 UILabels into another UIView with spacing zero between them, do not set a height constraint for this UIView and just set it in the centerY of the cellView and spaced from the ImageView
Tip: Only change constraint priorities if your UI is what you want even after the constraints are broken, unless of course you know what you're doing
For ur case no need to vertical hugging priority,just give
label.numberOfLines = 0
that works fine.
The question says you want to resize the label, but based on your code it seems like you want to do it by resizing the tableview rows. If that's the case, the correct place to set row height in a tableView is:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 80
}
Where "return 80" means you want table rows to be of height 80. Please let me know if I didn't understand your question right.
The label doesn't work because the storyboard is not very useful.
Set the var:
var imageView: UIImageView ={
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var textView: UITextView ={
let text = UITextView()
text.translatesAutoresizingMaskIntoConstraints = false
return text
}()
And you continue with other label.
You have to set constraints like this example:
self.addSubview(imageView)
self.addSubview(textView)
imageView.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor, constant: -10).active=true
imageView.leftAnchor.constraintEqualToAnchor(self.leftAnchor, constant:-10).active=true
imageView.widthAnchor.constraintEqualToConstant(70).active=true
imageView.heightAnchor.constraintEqualToConstant(70).active=true
label.topAnchor.constraintEqualToAnchor(self.topAnchor, constant: -5).active=true
label.leftAnchor.constraintEqualToAnchor(imageView.rightAnchor, constant: 3).active=true
label.widthAnchor.constraintEqualToAnchor(self.widthAnchor, constant -70).active=true
label.heightAnchor.constraintEqualToConstant(30).active=true
label2.topAnchor.constraintEqualToAnchor(label.bottomAnchor, constant: 5).active=true
label2.leftAnchor.constraintEqualToAnchor(imageView.rightAnchor, constant: 3).active=true
label2.widthAnchor.constraintEqualToAnchor(self.widthAnchor, constant -70).active=true
label2.heightAnchor.constraintEqualToConstant(30).active=true
And with this way also for the third label.

Using UIStackView with a UITextView with dynamic height

I just found out about UIStackView, and I'm trying to see if it can finally make it easier to lay out expanding content.
Specifically, I want to do the following:
|-- UITextView ------------|
| some dynamic text here, | I want the text view's height to change
| it could be short, or it | depending on the height of the text
| could be tall | (set later, in code)
|-- UIView ----------------|
|xxxxxxxxxxxxxxxxxxxxxxxxxx| I want this view to expand to fill the
|xxxxxxxxxxxxxxxxxxxxxxxxxx| vertical space.
|xxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxx| It should start right below the
|xxxxxxxxxxxxxxxxxxxxxxxxxx| bottom of the UITextView
|xxxxxxxxxxxxxxxxxxxxxxxxxx|
|--------------------------|
I think I know how to do this with regular constraints. Is it possible to do with UIStackView? I might be understanding the purpose of UIStackView, but I would love to be able to create layouts like this more easily.
When I put both of these into a UIStackView, Interface builder says "Needs constraints for: Y position or height" for both views.
Programmatically it looks like this:
#IBOutlet private var stackView: UIStackView!
{
didSet {
stackView.addArrangedSubview(titleTextView)
stackView.addArrangedSubview(UIView())
}
}
lazy var titleTextView: UITextView =
{
let textView = UITextView()
textView.isScrollEnabled = false
return textView
}()
You don’t need anything else for the UITextView to expand automatically. This shouldn’t complain about ambiguities.
You can add a stackview then inside that add a textview, then set scrollable false, it will automaticaly handle your problem
Calculate the text height in textview add a height constraint on uitextview say 20 and when you calculate the height just change the height constraint.Give the below view constraint of top 0 from textview and you issue will be resolved.
Calculate height using this function
let height = string.boundingRectWithSize(CGSizeMake(CGFloat.max,textviewwidth), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font!], context: nil).size.height
Try using an UILabel with lines = 0 instead of an UITextView.

ios autolayout dynamic UILabel height

Say I have three UILabels whose positions are like below:
[Label1] [Label2]
[Label3]
Label1 and Label2 are in the same row and Label3 is below them. All the labels will have a fixed width and will contain dynamic text, so their height will vary.
How do I make the Label3 10 points below the label which has a higher height using AutoLayout?
For example, if Label1's height is 100 points, Label2's height is 120 points (their Y positions are the same), then Label3 should be 10 points below Label2, but if Label1 is 120 points high and Label2 is 100 points high, then Label3 should be 10 points below Label1.
You simply make constraints between both Label3->Label1 and Label3->Label2. Use inequality constraints. There will be only one way to satisfy both!
You will also need a top constraint for Label3; its constant should be very small and its priority should be very low. This will give the two inequality constraints something to "aim at".
Here is an example. This as achieved entirely without code - the buttons have code to add text to the labels, of course, but the constraints are configured entirely in Interface Builder; the labels are resizing, and the bottom label is moving down, automatically. (You can construct the same layout in code if you want to, naturally.)
I suggest you to wrap top two labels to UIView and setup constraints so these labels fit all space inside that view. Then you simple add vertical spacing constraint to bottom label3 with constant = 10. In that case top view will have size of larger label and will satisfy your conditions
I thought this would be an interesting exercise so I create a little test project. The gist of the code is below. You can just copy/paste it in the standard Single View iOS template.
(Note that I use SnapKit for programmatic Auto Layout because it is so much simpler than the UIKit API. I find it even much simpler than doing things in Xcode.)
The result is exactly the same as Matt's great screencast.
// ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController
{
override func viewDidLoad() {
super.viewDidLoad()
let leftLabel = UILabel()
leftLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
leftLabel.userInteractionEnabled = true
view.addSubview(leftLabel)
leftLabel.numberOfLines = 0
leftLabel.text = "All the world's a stage, and all the men and women merely players: they have their exits and their entrances; and one man in his time plays many parts, his acts being seven ages."
leftLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.left.equalTo(self.view)
make.right.equalTo(self.view.snp_centerX)
}
let rightLabel = UILabel()
rightLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
rightLabel.userInteractionEnabled = true
view.addSubview(rightLabel)
rightLabel.numberOfLines = 0
rightLabel.text = "There is a tide in the affairs of men, Which taken at the flood, leads on to fortune. Omitted, all the voyage of their life is bound in shallows and in miseries. On such a full sea are we now afloat. And we must take the current when it serves, or lose our ventures."
rightLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.right.equalTo(self.view)
make.left.equalTo(self.view.snp_centerX)
}
let bottomView = UIView()
view.addSubview(bottomView)
bottomView.backgroundColor = UIColor.redColor()
bottomView.snp_makeConstraints { (make) -> Void in
make.height.equalTo(20)
make.left.right.equalTo(self.view)
make.top.greaterThanOrEqualTo(leftLabel.snp_bottom)
make.top.greaterThanOrEqualTo(rightLabel.snp_bottom)
}
}
#objc func addText(recognizer: UIGestureRecognizer) {
if let label = recognizer.view as? UILabel {
label.text = label.text! + " I like cheese."
}
}
}
Updated the code to add some additional text to the labels when tapped.
First of all remove height constraints and set all 3 labels vertical Content Compression Resistance Priority to 1000. This is the most important part.
Then add vertical space from Label3 to Label 1, and set instead of Equal, Greater Than or Equal with priority say 500. Add same space constraint to Label2.
Last add constraint from Label3 to Top = 0, but set priority to 1.

Resources