How to add constraint with dynamic width programmatically [duplicate] - ios

This question already has answers here:
How to add constraints programmatically using Swift
(19 answers)
Closed 6 years ago.
I'm new to iOS development. I want to build layout without storyboard or xib/nib. So I am trying to add constraints programmatically.
I have searched some tutorials about add constraints programmatically. But the view can't show correctly.
I'm trying this code in my ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let testView = UIView()
self.view.addSubview(testView)
// Add Constraints
self.view.translatesAutoresizingMaskIntoConstraints = false
testView.translatesAutoresizingMaskIntoConstraints = false
let top = NSLayoutConstraint(item: testView, attribute: .top, relatedBy: .equal
, toItem: self.view, attribute: .top, multiplier: 1, constant: 50.0)
let bottom = NSLayoutConstraint(item: testView, attribute: .bottom, relatedBy: .equal
, toItem: self.view, attribute: .bottom, multiplier: 1, constant: -50.0)
let leading = NSLayoutConstraint(item: testView, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: self.view, attribute: .leading, multiplier: 1, constant: 50.0)
let trailing = NSLayoutConstraint(item: testView, attribute: .trailing, relatedBy: .greaterThanOrEqual, toItem: self.view, attribute: .trailing, multiplier: 1, constant: -50.0)
self.view.addConstraints([top, bottom, leading, trailing])
}
Generally, they don't need to define the size of view or constraints about width and height in tutorials. But the views can be shown on their tutorial. In my case, my testView can't show it in the app even top, bottom, leading and trailing constraints have been set. Am I missed something? What's the problem?
one of the tutorials:
http://www.knowstack.com/swift-nslayoutconstraint-programatically-sample-code/
Edit:
Let's me explain more. I want to create a UIView that suitable for universal device. The UIView has top, bottom, leading and trailing constraints with constant: 10. So, I don't need to set size of UIView.
Expected Result (I am using draw tool to simulate the result)

This is an example of a view constraint to the bottom of the screen with height equal to 80:
var yourView = UIView()
yourView.translateAutoresizingMaskIntoConstraints = false
yourSuperView.addSubview(yourView)
// Pin the leading edge of yourView to the leading edge of the main view
yourView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
// Pin the trailing edge of yourView to the leading trailing edge
yourView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
// Pin the bottomedge of yourView to the margin's leading edge
yourView .bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
// The height of your view
yourView.heightAnchor.constraintEqualToConstant(80).active = true

You have two issues:
Do not set self.view.translatesAutoresizingMaskIntoConstraints = false. You only need to set translatesAutoresizingMaskIntoConstraints for the subview you are adding.
You are using .greaterThanOrEqual constraints instead of .equal constraints. The problem with that is that leaves a lot of wiggle room and you are getting values that cause your testView to have 0 width. If you change to .equal that will properly constrain the values.

Related

Sizing label with Autolayout

Description of the issue
I have a label to the left side of a plot view in an iOS App.
I have succeeded in rotatif the label 90 degrees, as pictured below, using the code copied in a following section :
However, as soon as I change the text to "Speed (Km/h):, the label becomes wider, as shown below :
Layout
There are no relevant contraints were set in interface builder. The only contraints set here are :
- vertical centering of the label
- plot view's right, top, and bottom edges stick to view's right, top, and bottom edges
The rest is set in code
Code
func setup(speedArray: [Float32]) {
//Convert speed to Km/h from m/s
let speedArrayKMH = speedArray.map({$0*3.6})
//Draw Chart
//This only styles the chart (colors, user interaction, etc...
//no code in this fuction that affects layout)
setupSpeedChart(speedArray: speedArrayKMH)
//
// Customise speed label
//
//Label text
chartTitleLabel.textAlignment = .center
//Text color
self.chartTitleLabel.textColor = BoxTextColor
//Rotate
chartTitleLabel.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi/2))
//Constrain
let C1 = NSLayoutConstraint(item: chartTitleLabel, attribute: .leadingMargin, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0)
let C2 = NSLayoutConstraint(item: chartTitleLabel, attribute: .trailingMargin, relatedBy: .equal, toItem: speedLineChart, attribute: .leading, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([C1, C2])
}
What I've tried
I've tried a number of construit and size combinations, including :
fixing the label's frame property (height and width)
adding a constraint for the label's width and height
nothing seems to work
Adding the following constraint for label width for example produces this :
let C3 = NSLayoutConstraint(item: chartTitleLabel, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 45)
there are few key thing missing
first you forgot to use translateAutoReaizingMaskIntoConstraints
go to storyboard select your label go to inspector under label there is autoshrink set it to minimum font size set the size to 11
then do the following in view didload
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myLabel.text = "speed in km"
myLabel.textAlignment = .center
myLabel.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi/2))
myLabel.translatesAutoresizingMaskIntoConstraints = false
let c1 = NSLayoutConstraint(item: myLabel, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 200)
let c2 = NSLayoutConstraint(item: myLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 60)
let c3 = NSLayoutConstraint(item: myLabel, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: -60)
let c4 = NSLayoutConstraint(item: myLabel, attribute: .centerY, relatedBy: .equal, toItem: myImageView, attribute: .centerY, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([c1, c2, c3, c4])
}
I have tried to update the text to new text with more character count than previous and I worked just fine
The issue lies in this part of the description of transform in Apple's docs: https://developer.apple.com/documentation/uikit/uiview/1622459-transform
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.
So, when you change the text of the label, your constraints are related to the untransformed frame.
The fix is to embed the label in a "container" UIView and constrain the container's width to the height of the label, and the height to the width of the label.
Set up your xib like this (I use contrasting background colors to make it easy to see frames):
Note that I've given the container view width and height constraints of 80. Edit each of those constraints and set them as "Placeholders":
We'll be setting new constraints in code, so these will be removed at build time, but will satisfy IB's layout checking.
In awakeFromNib() in your custom class, add the new width and height constraints and apply the rotation transform:
override func awakeFromNib() {
super.awakeFromNib()
// set HUGGING priority on title label to REQUIRED for both axis
chartTitleLabel.setContentHuggingPriority(.required, for: .horizontal)
chartTitleLabel.setContentHuggingPriority(.required, for: .vertical)
NSLayoutConstraint.activate([
// constrain title container view WIDTH equal to title label HEIGHT
// set the constant to add padding on left and right of rotated title label
// here it is set to 12, which gives 6-pts padding on each side
chartTitleContainerView.widthAnchor.constraint(equalTo: chartTitleLabel.heightAnchor, constant: 12.0),
// constrain title container view HEIGHT equal to title label WIDTH
chartTitleContainerView.heightAnchor.constraint(equalTo: chartTitleLabel.widthAnchor, constant: 0.0),
])
// rotate the title label
chartTitleLabel.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi/2))
// un-comment after development
// chartTitleLabel.backgroundColor = .clear
// chartTitleContainerView.backgroundColor = .clear
}
Now, in your setup() func (where, I'm assuming, you add the "plot view" as a subview), add the constraints for the plot view:
func setup(speedArray: [Float32]) {
//Convert speed to Km/h from m/s
let speedArrayKMH = speedArray.map({$0*3.6})
// assuming setupSpeedChart() creates speedLineChart view and adds it to self
//Draw Chart
//This only styles the chart (colors, user interaction, etc...
//no code in this fuction that affects layout)
setupSpeedChart(speedArray: speedArrayKMH)
NSLayoutConstraint.activate([
// constrain chart view LEADING to title container view TRAILING
speedLineChart.leadingAnchor.constraint(equalTo: chartTitleContainerView.trailingAnchor, constant: 0.0),
// constrain chart view top, bottom and trailing to self
speedLineChart.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
speedLineChart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
speedLineChart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
}
This is the result:
and with the "dev" colors turned off:
You can now change the text of the title label and it will remain centered vertically and horizontally, without changing the horizontal size / spacing.

UIScrollView dynamic height content (autolayout) not filling whole space

I am creating a small chat app in which I use a custom subclass of UIScrollView to present the messages. This app is just for practicing so I don't want to use a third party library. I am implementing this UIScrollView via autolayout, after reading the technical note 2154 by Apple and several tutorials explaining this, and my implementation is almost working but the content view of my UIScrollView doesn't seem to fill all the space available.
The code which presents the ScrollView is:
public class ChatView: UIScrollView {
private var contentView: UIView
...
// This get called by all the init methods. contentView is already created ( contentView = UIView() )
private func setupViews() {
self.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
let constraint1 = NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1.0, constant: 0.0)
let constraint2 = NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
let constraint3 = NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, toItem: contentView, attribute: .Width, multiplier: 1.0, constant: 0.0)
let constraint4 = NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal, toItem: contentView, attribute: .CenterX, multiplier: 1.0, constant: 0.0)
self.addConstraints([constraint1, constraint2, constraint3, constraint4])
self.layoutIfNeeded()
// Later, the messages are added to the contentView. I don't think is relevant to see the exact code (but I can post it if needed)
// Each message is added using autolayout and the constraints only reference the messages themselves and contentView
}
}
When I add a ChatView to my view controller (using storyboards), with its four sides pinned to views which are not in his hierarchy, the following problem happens:
In the image, the scrollView cannot be scrolled upwards any more. There seem to be a space which should be filled and isn't. If I scroll down, I have the exact same problem but the empty space is below the content. In the following images you can see that the contentView is smallest than the ChatView itself:
And the same view hierarchy but with the constraints shown:
In both images the view in the background is the ChatView and the selected one is the contentView. I haven't been able to figure why the content view doesn't cover the full ChatView space.
Thanks in advance!
I finally stumbled upon the answer while searching a different problem, in another stackoverflow question. The key is to open the storyboard and set in the container view controller "AdjustsScrollViewInsets" to "NO".
In code it's simply (inside the view controller):
self.automaticallyAdjustsScrollViewInsets = NO;

Programmatic Autolayout changes behaviour of a UIView

I have a controller where I add a subview programmatically. With the configuration of the subview I add autolayout constraints programmatically. Everthing is working except that the view doesn't react on touches if I add the constraints and even the set backgroundcolor is not displayed.
The buttonView should be displayed in the lower right corner of my parent view.
Any ideas what could be wrong?
Here is how I add my constraints:
private func configureAutolayoutConstraints(buttonView: UIView, parentView: UIView){
buttonView.translatesAutoresizingMaskIntoConstraints = false
let bottomConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Bottom, relatedBy: .Equal, toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: -130)
parentView.addConstraint(bottomConstraint)
let trailingConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Trailing, relatedBy: .Equal, toItem: parentView, attribute: .Trailing, multiplier: 1.0, constant: -90)
parentView.addConstraint(trailingConstraint)
}
Autolayout engine needs at least 4 constraints to determine the frame of view. You have applied bottom and trailing constraints only. You need either width+height OR leading+top constraint to make it work.

Badge view how to create with a full auto layout approach

It's been a while since I'm trying to create a custom view that has at the top right corner a simple badge.
The custom view is composed by 3 part.
In red there is the custom view itself, that contains:
in blue the container view
in green the badge view a simple UILabel
Container view and the badge view are sibling.
At the moment the container view contains a UIImageView
That view must fit those requirements:
Full auto layout approach
Made programmatically
The custom view must be aligned only considering the blue frame(the container view)
Why that? imagine that you need to position that view by aligning the top edge to the top edge of another view or button, wouldn't be nice if the only the content showed int he container is taken into account?
Here you can see how I set the constraints. The label is placed at the top right corner of the container view.
func setUpConstraint() {
var horContraints = [NSLayoutConstraint]()
horContraints.append(NSLayoutConstraint(item: containerView, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1, constant: 0))
horContraints.append(NSLayoutConstraint(item: containerView, attribute: .Trailing, relatedBy: .Equal, toItem: badge, attribute: .CenterX, multiplier: 1, constant: 0))
horContraints.append(NSLayoutConstraint(item: badge, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 0))
var verContraints = [NSLayoutConstraint]()
verContraints.append(NSLayoutConstraint(item: containerView, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0))
verContraints.append(NSLayoutConstraint(item: containerView, attribute: .Top, relatedBy: .Equal, toItem: badge, attribute: .CenterY, multiplier: 1, constant: 0))
verContraints.append(NSLayoutConstraint(item: badge, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
addConstraints(verContraints + horContraints)
containerView.addConstraint(NSLayoutConstraint(item: containerView, attribute: .Height, relatedBy: .Equal, toItem: containerView, attribute: .Width, multiplier: 1, constant: 0))
}
The container view has also an aspect ratio constraint to keep a square size.
As you can see from the picture everything seems to be fine, except for the fact that when I try to constraint the custom view to the center of its superview, it seems misaligned, because I want the view to be centered respect to the container view (the one with the image). The badge is a kind of decoration such as a shadow and I don't want it to be consider.
To align it correctly I'm trying to override the alignment rect by adding an insets that would "cut" half the label size.
override func alignmentRectInsets() -> UIEdgeInsets {
let badgeSize = badge.bounds.size
return UIEdgeInsets(top: badgeSize.height / 2, left: 0, bottom: 0, right: badgeSize.width / 2)
}
I tried also different configurations but I never was able to fit in the wanted position
If I try to use the other 2 methods alignmentRectForFrame and frameForAlignmentRect (deleting alignmentRectInsets) they are never be called.
Here is what I'd like to obtain:
I've created a little sample code
If the problem is that you want the other view (the "content view", showing the image) to be centered in the ultimate superview, then simply make centering constraints (center x to center x, center y to center y) between the superview and the content view. No law says that a constraint has to be between a view and its direct superview; you can make constraints between any pair of views (that is one of the wonderful things about constraints).
I made a quick mock-up that looks a lot like your "here's what I'd like to obtain" image:
As you can see, Moe (the middle Pep Boy, in the middle of the image) is exactly centered in the superview (shown by the green lines).
For simplicity, the entire interface is created in code, so that I can show you the whole thing. Here it is:
// create the custom view
let customView = UIView()
customView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(customView)
customView.backgroundColor = UIColor.redColor()
NSLayoutConstraint.activateConstraints([
customView.widthAnchor.constraintEqualToConstant(100),
customView.heightAnchor.constraintEqualToConstant(100),
])
// add the content view (image) to the custom view
let contentView = UIImageView(image: UIImage(named:"pep.jpg")!)
contentView.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(contentView)
NSLayoutConstraint.activateConstraints([
contentView.leadingAnchor.constraintEqualToAnchor(customView.leadingAnchor),
contentView.bottomAnchor.constraintEqualToAnchor(customView.bottomAnchor),
contentView.heightAnchor.constraintEqualToAnchor(customView.heightAnchor, constant: -10),
contentView.widthAnchor.constraintEqualToAnchor(customView.widthAnchor, constant: -10)
])
// add the badge (label) to the custom view
let badge = UILabel()
badge.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(badge)
badge.backgroundColor = UIColor.greenColor().colorWithAlphaComponent(0.5)
badge.font = UIFont(name: "GillSans", size: 14)
badge.textAlignment = .Center
badge.text = "567"
NSLayoutConstraint.activateConstraints([
badge.centerXAnchor.constraintEqualToAnchor(contentView.trailingAnchor),
badge.centerYAnchor.constraintEqualToAnchor(contentView.topAnchor),
])
// position the whole thing with respect to the content view
NSLayoutConstraint.activateConstraints([
contentView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor),
contentView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor),
])
But this is not a complete answer to your question. You should now be asking: Yes, but what went wrong when I tried to use the alignmentRectInsets? The answer is: you forgot that the alignmentRectInsets affect alignment with internal views as well as external views. In other words, you certainly can use that approach instead, but then you must adjust the position of the content view accordingly.
So, here's a rewrite in which I use the alignmentRectInsets. First, I'll define a custom view subclass for our custom view:
class AlignedView : UIView {
override func alignmentRectInsets() -> UIEdgeInsets {
return UIEdgeInsetsMake(10, 0, 0, 10)
}
}
Now here's the rewrite. I've put a star (*) next to all the lines that have changed from the previous example:
// create the custom view
let customView = AlignedView() // *
customView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(customView)
customView.backgroundColor = UIColor.redColor()
NSLayoutConstraint.activateConstraints([
customView.widthAnchor.constraintEqualToConstant(100),
customView.heightAnchor.constraintEqualToConstant(100),
])
// add the content view (image) to the custom view
let contentView = UIImageView(image: UIImage(named:"pep.jpg")!)
contentView.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(contentView)
NSLayoutConstraint.activateConstraints([
contentView.leadingAnchor.constraintEqualToAnchor(customView.leadingAnchor),
contentView.bottomAnchor.constraintEqualToAnchor(customView.bottomAnchor),
contentView.heightAnchor.constraintEqualToAnchor(customView.heightAnchor), // *
contentView.widthAnchor.constraintEqualToAnchor(customView.widthAnchor) // *
])
// add the badge (label) to the custom view
let badge = UILabel()
badge.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(badge)
badge.backgroundColor = UIColor.greenColor().colorWithAlphaComponent(0.5)
badge.font = UIFont(name: "GillSans", size: 14)
badge.textAlignment = .Center
badge.text = "567"
NSLayoutConstraint.activateConstraints([
badge.centerXAnchor.constraintEqualToAnchor(contentView.trailingAnchor),
badge.centerYAnchor.constraintEqualToAnchor(contentView.topAnchor),
])
// position the whole thing with respect to the custom view
NSLayoutConstraint.activateConstraints([
customView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor), // *
customView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor), // *
])
That gives exactly the same visual result as before.

Swift autolayout multiple views/labels

I want to programmatically divide my screen into three parts. Each part shall be a label with a backgroundColor and the width is set to be:
// Get the device width
self.view.bounds.width
The height must be dynamically. An example is in the image below. Each color is one part. The first part (the red part) must be attached to the top and the last part (the orange) must be attached to the bottom. And dependent on the label size inside each part the height shall be adjusted.
The blue part is always 100% and dependent of the red and orange part it subtracts from the blue part.
Anyone have any ideas of how to achieve this? Appreciate help!
To do this is best to use AutoLayout, using NSLayoutConstraints. Here's some sample code (at the moment this is all in viewDidLoad, which probably isn't the best thing to do, but I'll leave it up to you to organise the code.)
override func viewDidLoad() {
super.viewDidLoad()
// 1.
var topView: UIView = self.view
// Have as many labels as you want!
for i in 0..<numberOfLabels {
let label = UILabel()
// 2.
label.numberOfLines = 0
label.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(label)
// 3.
if i == 0 || i == numberOfLabels - 1 {
label.setContentHuggingPriority(UILayoutPriority.abs(1000), forAxis: UILayoutConstraintAxis.Vertical)
}
let widthConstraint = NSLayoutConstraint(item: label, attribute: .Width, relatedBy: .Equal, toItem: self.view, attribute: .Width, multiplier: 1.0, constant: 0.0)
// 4.
let heightConstraint = NSLayoutConstraint(item: label, attribute: .Height, relatedBy: .GreaterThanOrEqual, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 20.0)
// 5.
let attribute: NSLayoutAttribute = i == 0 ? .Top : .Bottom
let topConstraint = NSLayoutConstraint(item: label, attribute: .Top, relatedBy: .Equal, toItem: topView, attribute: attribute, multiplier: 1, constant: 0)
// 6.
if i == numberOfLabels - 1 {
let bottomConstraint = NSLayoutConstraint(item: label, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1, constant: 0)
self.view.addConstraint(bottomConstraint)
}
label.addConstraint(heightConstraint)
self.view.addConstraint(widthConstraint)
self.view.addConstraint(topConstraint)
topView = label
}
}
1. topView is the view that the label's NSLayoutAttribute.Top will be related to. For example: the first label's top is related to its container view; the second label's top is related to the first label; the third label's top is related to the second view, and so on.
2. Set the number of lines to zero to allow for as many lines as required by label.text.
3. Set the content hugging priority of the top and bottom labels to 1000. This will cause the top and bottom labels to 'hug' the text because middle label(s), by default, have a hugging priority of 250.
4. Create an NSLayoutConstraint that sets the label to be higher than 20 points because for preferredMaxLayoutWidth to be set automatically the width and height of the label must be explicitly defined by NSLayoutConstraints.
5. This selects the attribute on topView that will pin the label to the topView. For example: if creating the first label, its top is pinned to the top of the container view. Otherwise, the top of the label is pinned to the bottom of the label above it.
6. If it's the last label, pin its bottom edge to the bottom of the container view.
Here's the result:
Hope that answers your question.

Resources