Using NSLayoutAnchor to create constraints between two labels? - ios

I'm creating a view programmatically and need to set constraints between two labels. I recently just discovered NSLayoutAnchor and feel it would be a good choice to use it but I'm unsure how to create constraints between two different things (ie labels, imageViews, etc). I know a general setup will look something like this:
let codedLabel:UILabel = UILabel()
codedLabel.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
codedLabel.textAlignment = .center
codedLabel.text = alertText
codedLabel.numberOfLines=1
codedLabel.textColor=UIColor.red
codedLabel.font=UIFont.systemFont(ofSize: 22)
codedLabel.backgroundColor=UIColor.lightGray
self.contentView.addSubview(codedLabel)
codedLabel.translatesAutoresizingMaskIntoConstraints = false
codedLabel.heightAnchor.constraint(equalToConstant: 200).isActive = true
codedLabel.widthAnchor.constraint(equalToConstant: 200).isActive = true
codedLabel.centerXAnchor.constraint(equalTo: codedLabel.superview!.centerXAnchor).isActive = true
codedLabel.centerYAnchor.constraint(equalTo: codedLabel.superview!.centerYAnchor).isActive = true
How would you set up constraints in between two labels?

Let's say you have two UIViews you wish to lay out horizontally in one of two ways:
View #1 is 20 points from the leading edge of the superview's margin or safe area and View #2 is another 20 points from view #1. (Think of a left-justified row of buttons.)
Both views are to be centered, with equal spacing before/between/after each view. (Think of two buttons spaced equally apart and centered.)
For example #1 the code would be:
// #1
let view1 = UIView()
view1.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(view1)
let view2 = UIView()
view2.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(view2)
// #2
let margins = view.layoutMarginsGuide
view.addLayoutGuide(margins)
view1.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 20).isActive = true
// #3
view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor, constant: 20).isActive = true
// #4
view1.widthAnchor.constraint(equalToConstant: 100).isActive = true
view2.widthAnchor.constraint(equalToConstant: 100).isActive = true
Comment #1: Note that you do not need to give each view a frame, but you do need to set the auto-resizing mask to false. This is a mistake many new coders forget.
Comment #2: All UIViewController main views have a layoutMarginGuide that yield standard margins both vertically and horizontally (and in iOS 11, particularly for iPhone X, there is a new safeAreaLayoutGuide for vertical alignment). I've set the leading edge of view1 to be the leading edge of the margin with an additional constant of 20 points from it.
Comment #3: Just like I related view1 to the margin, I'm relating view2 to view1, but this time the leading edge ofview2is 20 points from theview1` trailing edge.
Comment #4: The last thing you need to do for horizontal placement is to give each view a width. In this case I wanted both to be 100 points.
Example #2 pretty much uses the same code as example #1, so I'll note the key differences only:
let view1 = UIView()
view1.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(view1)
let view2 = UIView()
view2.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(view2)
let margins = view.layoutMarginsGuide
view.addLayoutGuide(margins)
// #1
let spacer1 = UILayoutGuide()
view.addLayoutGuide(spacer1)
let spacer2 = UILayoutGuide()
view.addLayoutGuide(spacer2)
let spacer3 = UILayoutGuide()
view.addLayoutGuide(spacer3)
// #2
spacer.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
view1.leadingAnchor.constraint(equalTo: spacer1.trailingAnchor).isActive = true
spacer2.leadingAnchor.constraint(equalTo: view1.trailingAnchor).isActive = true
view2.leadingAnchor.constraint(equalTo: spacer2.trailingAnchor).isActive = true
spacer3.leadingAnchor.constraint(equalTo: view2.trailingAnchor).isActive = true
spacer3.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
// #3
view1.widthAnchor.constraint(equalToConstant: 100).isActive = true
view2.widthAnchor.constraint(equalToConstant: 100).isActive = true
spacer1.widthAnchor.constraint(equalTo: spacer2.widthAnchor).isActive = true
spacer2.widthAnchor.constraint(equalTo: spacer3.widthAnchor).isActive = true
Comment #1: In iOS 9 Apple introduced the concept of UILayoutGuides. Before this, to create "spacers" you had to actually create an invisible UIView and add it as a subview (with all the overhead associated with it). Layout guides "act" like views but do not have that overhead.
Comment #2: The horizontal sequence if "margin...spacer1...view1...spacer2...view2...spacer3...margin". Note that I'm not using any constants, as I wish to let the layout engine give equal spacing.
Comment #3: While I am giving width values for both views, I am not with the spacers. Instead, I am declaring their widths to be equal.
Please note that I've only worked with horizontal constraints. For auto layout to work, you also need to declare vertical constraints too. Most of the concepts are the same... instead of leading/trailing, centerX, and width anchors you have top/bottom, centerY, and height ones. BUT! Starting with iOS 11 you now have a safeAreaLayoutGuide that you should be using instead of layoutMarginsGuide. This only applies to vertical! That's why I separated things. Here's the code I use to work with vertical alignment:
let layoutGuideTop = UILayoutGuide()
let layoutGuideBottom = UILayoutGuide()
view.addLayoutGuide(layoutGuideTop)
view.addLayoutGuide(layoutGuideBottom)
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
layoutGuideTop.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0).isActive = true
layoutGuideBottom.bottomAnchor.constraintEqualToSystemSpacingBelow(guide.bottomAnchor, multiplier: 1.0).isActive = true
} else {
layoutGuideTop.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
layoutGuideBottom.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
Now you have layoutGuideTop and layoutGuideBottom that should work regardless of what iOS version is running.

Related

StackView constraints with UIViews

I'm trying to setup a stackview in the middle of the screen with a padding of 20 to the left an right. Inside, I want to place two custom UIViews, but I don't quit understand how to do it. I tried giving the UIViews their respective Height's and Width's but I got nothing.
I believe the stack view has all the correct constraints. Here's the code:
func setupTeamViews() {
view.addSubview(teamsStackView)
teamsStackView.distribution = .fill
teamsStackView.axis = .vertical
teamsStackView.spacing = 20
teamsStackView.alignment = .fill
NSLayoutConstraint.activate([
teamsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
teamsStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
teamsStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
teamsStackView.heightAnchor.constraint(equalToConstant: 420)
])
let height = CGFloat((teamsStackView.frame.height / 2) - 20)
let width = CGFloat((teamsStackView.frame.width - 20))
firstTeamView = ATTeamView(width: width, height: height)
firstTeamView.changeColor(color: .lightBlue)
firstTeamView.setTeamName(name: "Tobias")
firstTeamView.setNewPoints(points: "0")
secondTeamView = ATTeamView(width: width, height: height)
secondTeamView.changeColor(color: .white)
secondTeamView.setTeamName(name: "Valen")
secondTeamView.setNewPoints(points: "0")
teamsStackView.addArrangedSubview(firstTeamView)
teamsStackView.addArrangedSubview(secondTeamView)
}
How does a stackview work with UIview's? As far I understand, UIViews don't have intrinsicContentSize, but I don't know how to deal with that.
In stead of doing it programmatically you can add two container views attach them to your view controller, and then active them with the alpha.
This will make constraints easier and you can still program anything extra you would like
myView1.alpha = 1 // activate
myView2.alpha = 0 // deactivate
My code wasn't working because I've forgotten to set teamsStackView.translatesAutoresizingMaskIntoConstraints = false

Can't get UIVew sizes

I add UIViews dynamically in code. There are a lot of views I want to add. Here is one for example:
let typeImage = UIImageView()
typeImage.translatesAutoresizingMaskIntoConstraints = false
rootView.addSubview(typeImage)
typeImage.widthAnchor.constraint(equalToConstant: 28).isActive = true
typeImage.heightAnchor.constraint(equalToConstant: 28).isActive = true
typeImage.leftAnchor.constraint(equalTo: rootView.leftAnchor, constant: 4).isActive = true
typeImage.topAnchor.constraint(equalTo: rootView.topAnchor, constant: 4).isActive = true
typeImage.backgroundColor = gh.myRed
typeImage.image = UIImage(named: "ic_question_bej")
I want to count sum of all elements that I add and finally set this counted value to my root view height. The problem is that
typeImage.frame.size.height
typeImage.bounds.size.height
always return 0.0
So how can I get height in CGFloat of newly added UIView via code?
Maybe I should set height of rootview to wrap all my subview in another way?
Edit
Xcode loads view with some delay and if you will try to get view size immediately after adding it to parent you will get 0.0 sizes.
You have to call yourCustomView.layoutIfNeeded() before cheking sizes.
Thanks Chenjtc for right Answer.
you can add the code like this:
typeImage.layoutIfNeeded()
before the code :
typeImage.frame.size.height
typeImage.bounds.size.height
constraint will not layout immediately, you should use layoutIfNeeded to layout the view, then you can get th frame
If you want the sum of multiple views height in CGFloat you can do it like this :
let viewHeight = self.view.frame.height //this in CGFloat
let sumHeight = viewHeight + ... other UIViews
print(sumHeight) // 568.0 for iPhone SE
or if you want to set your new UIView height you can do it by changing :
typeImage.heightAnchor.constraint(equalToConstant: 28).isActive = true
//
To :
typeImage.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 1).isActive = true

UIScrollView Subviews not expanding to fill width (Autolayout)

I'm using the following code to constrain a view to the left and right anchors of a parent UIScrollView.
Despite the right anchor and the left anchor being set to the ScrollView's left and right anchors, the view does not expand to fill the scrollview.
Note: The gray background in this image is the UIScrollView's background, so I know that's properly constrained to its parent view.
Code:
self.wtfView.translatesAutoresizingMaskIntoConstraints = false
self.wtfView.backgroundColor = UIColor.orange
self.wtfView.topAnchor.constraint(equalTo: self.passwordField.bottomAnchor, constant: 40.0).isActive = true
self.wtfView.leftAnchor.constraint(equalTo: self.containerView.leftAnchor, constant: 40.0).isActive = true
self.wtfView.rightAnchor.constraint(equalTo: self.containerView.rightAnchor, constant: 40.0).isActive = true
self.wtfView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
self.wtfView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor, constant: 40.0).isActive = true
https://imgur.com/a/U88iW
Edit:
The following code works correctly, but I would prefer to use the left+right anchor technique to specify the width, and not at a width constraint. Shouldn't that be possible?
self.wtfView.translatesAutoresizingMaskIntoConstraints = false
self.wtfView.backgroundColor = UIColor.orange
self.wtfView.topAnchor.constraint(equalTo: self.passwordField.bottomAnchor, constant: 40.0).isActive = true
self.wtfView.leftAnchor.constraint(equalTo: self.containerView.leftAnchor, constant: 40.0).isActive = true
self.wtfView.widthAnchor.constraint(equalTo: self.containerView.widthAnchor, constant: -80.0).isActive = true //THE DIFFERENT ONE
self.wtfView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
self.wtfView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor, constant: 040.0).isActive = true
The reason for this is that the contentView of the UIScrollView still doesn't know that you want it to take up the width of it's parentView.
You can fix this by adding the following constraint in iOS11:
self.containerView.contentLayoutGuide.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
This says "Hey, I want you to lock the content Width to the width of the superview.
Pre iOS 11 you can simply constrain a subview to both the parent view's left and right anchors AND the content view's left and right anchors.
Like so:
self.wtfView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 40.0).isActive = true
self.wtfView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 40.0).isActive = true
Much like, Aleksei's recommendation you are now constraining the width to a rigid value ( the width of the parent view ), and the scrollview will use that to decide the width of the scrollview.
may be try to provide:
self.wtfView.widthAnchor.constraint(equalTo: self.containerView.widthAnchor, constant: -40.0).isActive = true

Swift how to set UIView's height constraint based on it's content

I have a UIView in my swift code
let profile_inf_wrapper: UIView = {
let v = UIView()
v.backgroundColor = .red
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
profile_inf_wrapper.topAnchor.constraint(equalTo: view.topAnchor, constant:64).isActive = true
profile_inf_wrapper.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
profile_inf_wrapper.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
profile_inf_wrapper.heightAnchor.constraint(equalToConstant: view.frame.height/4).isActive = true
backgroundImageView.topAnchor.constraint(equalTo: profile_inf_wrapper.topAnchor).isActive = true
backgroundImageView.leftAnchor.constraint(equalTo: profile_inf_wrapper.leftAnchor).isActive = true
backgroundImageView.rightAnchor.constraint(equalTo: profile_inf_wrapper.rightAnchor).isActive = true
backgroundImageView.bottomAnchor.constraint(equalTo: profile_inf_wrapper.bottomAnchor).isActive = true
profileImage.centerYAnchor.constraint(equalTo: profile_inf_wrapper.centerYAnchor).isActive = true
profileImage.leftAnchor.constraint(equalTo: view.leftAnchor, constant:25).isActive = true
profileImage.widthAnchor.constraint(equalToConstant: 110).isActive = true
profileImage.heightAnchor.constraint(equalToConstant: 110).isActive = true
usernameLabel.topAnchor.constraint(equalTo: profile_inf_wrapper.topAnchor, constant:40).isActive = true
usernameLabel.leftAnchor.constraint(equalTo: profileImage.rightAnchor, constant:20).isActive = true
countryIcon.topAnchor.constraint(equalTo: usernameLabel.bottomAnchor, constant:10).isActive = true
countryIcon.leftAnchor.constraint(equalTo: profileImage.rightAnchor, constant:20).isActive = true
countryIcon.widthAnchor.constraint(equalToConstant: 25).isActive = true
countryIcon.heightAnchor.constraint(equalToConstant: 25 ).isActive = true
countryName.topAnchor.constraint(equalTo: usernameLabel.bottomAnchor, constant:5).isActive = true
countryName.leftAnchor.constraint(equalTo: countryIcon.rightAnchor, constant:10).isActive = true
countryName.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
All these elements are the subviews of profile_inf_wrapper.Sometimes view.frame.height/4 is too small and i want to be able to resize the UIView based on it's content
There's a property in UIView called intrinsicContentSize. It returns the smallest size that the view would need show all of it's content.
While the default implementation is not very useful because a UIView doesn't have any content on it's own, all of the default subclasses implement it.
A UILabel will return a size that fits the text perfectly, and a UIButton will return a size that fits it's contents plus whatever spacing you've added. You get the gist of it.
You can take advantage of this property by only constraining either width or height of a view, not both. If you constrain the width of a UILabel and add more text, it will grow vertically.
Finally, when you add subviews to a UIView, and you add constraints to both margins of an axis (top and bottom or left and right), as long as there's a "chain" of constraints and views, and the view doesn't have any constraints on the size, it will expand to fit.
For example, if you have a view with a label and a button, vertically arranged, if the label is constrained to the top, then constrained to the button, and the button is constrained to the bottom, as long as the container view doesn't have a height constraint, it will expand to fit the two views plus the margins perfectly.
Your goal should always be to use the least amount of constraints to express your design, without removing useful constraints. Make sure you take advantage of the intrinsicContentSize.
For setting the height of uiview dynamically you have to add height/bottom constraint to the view in your problem it might be
profile_inf_wrapper.heightAnchor.constraint(equalToConstant: view.frame.height/4+countryName.frame.size.height).isActive = true
you also need the view size to fit to get actual updated size
like
countryName.sizeToFit()
And then update layout if needed to get all affect
The first thing you want to do is make a reference to the height constraint of profile_inf_wrapper.
var profile_inf_wrapper_height_constraint: NSLayoutConstraint?
I don't know the details of your content, but when the view needs resized, you can check that with a conditional in your viewController,
if contentRequiresResizing {
profile_inf_wrapper_height_constraint.constant = view.frame.width/3
else {
profile_inf_wrapper_height_constraint.constant = view.frame.width/4
}
By referencing constraints, it allows you to support dynamic UI changes easily.
As a side note, I would recommend renaming your UIView variable name so that the reference constraint isn't so long. The Swift 3 API guidelines also support lowerCamelCase, as opposed to underscore naming.

UIScrollView how do you constrain a sub view that acts a container to all the other views?

so as you will see below I have a scrollview and I want to add it the the UIViewControllers root view. When I have it constrained to the top, right, bottom, and left I expect to see the red color take up the whole screen. This obviously works, but I want to add a subview to the scrollview that will wrap all the child views. How would I go about doing that?
I have added the view and I have set the same constraints except this time they are set from the wrapper view to the bounds of the UIScrollView, and the blue background color doesn't show anywhere. Also feel free to point out if this is a bad idea, but I thought I could just have it be constrained to the bottom and it will automatically extend the scrollviews content size as needed. This seems to work when I had all the subviews in the scrollview without a wrapper and the last view would extend the content size.
scrollView = UIScrollView(frame: view.bounds)
scrollView?.showsVerticalScrollIndicator = true
scrollView?.backgroundColor = .red
scrollView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView!)
scrollView?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView?.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView?.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
//setup wrapper view
let subviewWrapper = UIView()
subviewWrapper.translatesAutoresizingMaskIntoConstraints = false
scrollView?.addSubview(subviewWrapper)
subviewWrapper.backgroundColor = .blue
subviewWrapper.topAnchor.constraint(equalTo: (scrollView?.topAnchor)!).isActive = true
subviewWrapper.leftAnchor.constraint(equalTo: (scrollView?.leftAnchor)!).isActive = true
subviewWrapper.rightAnchor.constraint(equalTo: (scrollView?.rightAnchor)!).isActive = true
subviewWrapper.bottomAnchor.constraint(equalTo: (scrollView?.bottomAnchor)!).isActive = true
Actually this is a very good idea. I always set up my scrollViews this way. I usually call the view contentView, but it is the same idea.
You're almost there. You haven't yet given Auto Layout anything to go on to figure out the size of your subviewWrapper. The constraints you've set so far pin the subviewWrapper to the edges of the scrollView's content area, but this just establishes the fact that as the subviewWrapper grows, the content size of the scrollView will expand. Currently your subviewWrapper has 0 width and 0 height which is why you see no blue.
Below are 3 examples of how you might establish the size of your subviewWrapper.
Note: Each of the following examples is completely independent. Look at each one separately and as you try them, remember to delete the constraints added by the previous example.
Example 1: Make subviewWrapper 1000 x 1000:
Set constraints to make your subviewWrapper 1000 x 1000 and you will see the blue and it will scroll in both directions.
subviewWrapper.widthAnchor.constraint(equalToConstant: 1000).isActive = true
subviewWrapper.heightAnchor.constraint(equalToConstant: 1000).isActive = true
Example 2: Vertical only scrolling with content size 2X of scrollView height:
If you set the width of your subviewWrapper to be equal to the width of the scrollView then it will only scroll vertically. If you set the height of subviewWrapper to 2X the height of scrollView, then your blue area will be twice the height of the scrollView.
subviewWrapper.widthAnchor.constraint(equalTo: scrollView!.widthAnchor, multiplier: 1.0).isActive = true
subviewWrapper.heightAnchor.constraint(equalTo: scrollView!.heightAnchor, multiplier: 2.0).isActive = true
Example 3: Size of subviewWrapper set by its subviews:
You can also establish the size of your subviewWrapper by adding subviews to it that are fully specified in size and connected in a chain from the top of subviewWrapper to the bottom, and from side to side. If you do this, Auto Layout will have enough information to compute the size of your subviewWrapper
In this example, I've added a yellow 600 x 600 square to the subviewWrapper and set it 100 points from each edge. Without having explicitly set a size for subviewWrapper, Auto Layout can figure out that it is 800 x 800.
let yellowSquare = UIView()
yellowSquare.translatesAutoresizingMaskIntoConstraints = false
yellowSquare.backgroundColor = .yellow
subviewWrapper.addSubview(yellowSquare)
yellowSquare.widthAnchor.constraint(equalToConstant: 600).isActive = true
yellowSquare.heightAnchor.constraint(equalToConstant: 600).isActive = true
yellowSquare.topAnchor.constraint(equalTo: subviewWrapper.topAnchor, constant: 100).isActive = true
yellowSquare.leadingAnchor.constraint(equalTo: subviewWrapper.leadingAnchor, constant: 100).isActive = true
yellowSquare.trailingAnchor.constraint(equalTo: subviewWrapper.trailingAnchor, constant: -100).isActive = true
yellowSquare.bottomAnchor.constraint(equalTo: subviewWrapper.bottomAnchor, constant: -100).isActive = true

Resources