Easiest way for assigning constraints to a view in swift? - ios

I use the following method to add a view as subview and adding its constraints programatically.
This is how I create the view:
// In class
let view: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
And this is how I add it's constraints:
addSubview(view)
view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
view.heightAnchor.constraint(equalToConstant: 100).isActive = true
Is there any method in which I can achieve this easier or with less lines of code. I need constraints to be created programatically and I don't recommend using another library just for this purpose.

It's not fewer lines of code, but rather than .isActive = true, line-by-line, I find activate to be a little cleaner:
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: leadingAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor),
view.widthAnchor.constraint(equalToConstant: 100),
view.heightAnchor.constraint(equalToConstant: 100)
])
Or, if you're doing this a lot, write your own extension:
extension UIView {
func activate(leading: NSLayoutAnchor<NSLayoutXAxisAnchor>? = nil,
trailing: NSLayoutAnchor<NSLayoutXAxisAnchor>? = nil,
top: NSLayoutAnchor<NSLayoutYAxisAnchor>? = nil,
bottom: NSLayoutAnchor<NSLayoutYAxisAnchor>? = nil,
centerX: NSLayoutAnchor<NSLayoutXAxisAnchor>? = nil,
centerY: NSLayoutAnchor<NSLayoutYAxisAnchor>? = nil,
width: CGFloat? = nil,
height: CGFloat? = nil) {
translatesAutoresizingMaskIntoConstraints = false
if let leading = leading { leadingAnchor.constraint(equalTo: leading).isActive = true }
if let trailing = trailing { trailingAnchor.constraint(equalTo: trailing).isActive = true }
if let top = top { topAnchor.constraint(equalTo: top).isActive = true }
if let bottom = bottom { bottomAnchor.constraint(equalTo: bottom).isActive = true }
if let centerX = centerX { centerXAnchor.constraint(equalTo: centerX).isActive = true }
if let centerY = centerY { centerYAnchor.constraint(equalTo: centerY).isActive = true }
if let width = width { widthAnchor.constraint(equalToConstant: width).isActive = true }
if let height = height { heightAnchor.constraint(equalToConstant: height).isActive = true }
}
}
And then you can do it with one line of code:
view.activate(leading: leadingAnchor, trailing: trailingAnchor, width: 100, height: 100)

You could use visual format language. In doing so you gain conciceness but lose clarity to developers who don't understand the syntax.
If you want to setup the above constraints for a view using VFL, the code would be as follows.
var constraints = [NSLayoutConstraint]()
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[view(100)]|", options: [], metrics: nil, views: ["view":view])
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:[view(100)]", options: [], metrics: nil, views: ["view":view])
NSLayoutConstraint.activate(constraints)
As you can see, for such a simple set of constraints, you don't gain a lot. However, consider you have to build a complex UI with many constraints on different objects, all relating to eachother, VFL would make doing so far easier and more concise.
VFL is very much a love it or hate it kind of thing, though I would recommend doing some reading on it so you can make your informed decision. This RayWendelich guide is particularly useful.

I wrote down a few extensions that I think is going to help all developers.
extension UIView {
func addView(view: UIView, top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets? = .zero, width: CGFloat?, height: CGFloat?) {
view.translatesAutoresizingMaskIntoConstraints = false
if !self.subviews.contains(view) {
addSubview(view)
}
if let top = top {
view.topAnchor.constraint(equalTo: top, constant: padding?.top ?? 0).isActive = true
}
if let leading = left {
view.leadingAnchor.constraint(equalTo: leading, constant: padding?.left ?? 0).isActive = true
}
if let bottom = bottom {
view.bottomAnchor.constraint(equalTo: bottom, constant: -(padding?.bottom ?? 0)).isActive = true
}
if let trailing = right {
view.trailingAnchor.constraint(equalTo: trailing, constant: -(padding?.right ?? 0)).isActive = true
}
if let width = width, width != 0 {
view.widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height, height != 0 {
view.heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
func anchorSizeTo(width: NSLayoutDimension?,and height: NSLayoutDimension?) {
if let width = width {
self.widthAnchor.constraint(equalTo: width).isActive = true
}
if let height = height {
self.heightAnchor.constraint(equalTo: height).isActive = true
}
}
func anchorCenterTo(x: NSLayoutXAxisAnchor?, y: NSLayoutYAxisAnchor?,with padding: CGPoint = .zero) {
if let anchor = x {
self.centerXAnchor.constraint(equalTo: anchor, constant: padding.x).isActive = true
}
if let anchor = y {
self.centerYAnchor.constraint(equalTo: anchor, constant: padding.y).isActive = true
}
}
}
Usage:
superView.addView(view: subView, top: upperView.bottomAnchor, leading: leftView.trailingAnchor, bottom: superView.bottom, trailing: rightView.trailingAnchor, padding: UIEdgeInsetsMake(12, 12, 12, 12), width: 0, height: nil)

NSLayoutAnchor class has made writing AutoLayout code much easier but still it's verbose and repetitive. You can create extension on UIView and add wrapper around AutoLayout which can be used from all UIViewControllers. For example I have added methods for size and pinning edges below
extension UIView {
func size(width: CGFloat, height: CGFloat) {
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: width),
self.heightAnchor.constraint(equalToConstant: height)
])
}
func edges(_ edges: UIRectEdge, to view: UIView, offset: UIEdgeInsets) {
if edges.contains(.top) || edges.contains(.all) {
self.topAnchor.constraint(equalTo: view.topAnchor, constant: offset.top).isActive = true
}
if edges.contains(.bottom) || edges.contains(.all) {
self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: offset.bottom).isActive = true
}
if edges.contains(.left) || edges.contains(.all) {
self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset.left).isActive = true
}
if edges.contains(.right) || edges.contains(.all) {
self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset.right).isActive = true
}
}
}
now you can set constraints for the your view using just 2 lines
view.edges([.left, .right], to: self.view, offset: .zero)
view.size(width: 100, height: 100)

Besides from NSLayoutConstraint.activate method that Rob mentioned, you can use a more general approach to eliminate multiple .isActive = true statements:
[
view.leadingAnchor.constraint(equalTo: leadingAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor),
view.widthAnchor.constraint(equalToConstant: 100),
view.heightAnchor.constraint(equalToConstant: 100),
].forEach {$0.isActive = true}
The provided inline function(closure) {$0.isActive = true} will be called for each element of the array.
$0 here is a shorthand parameter name. In our case it holds reference to an array element, a NSConstraint object.

Related

Align UIImage vertically center

I am trying to align an image vertically central using Swift. I understand you do this by using constraints, however I've been unable to get this to work.
func getLogo() {
let logo = UIImage(named: "LogoWhite")
let logoView = UIImageView(image: logo)
logoView.contentMode = .scaleAspectFit
self.addSubview(logoView)
}
If you don't want to use constraints (I personally do not like them) you can check for container center and put your UIImageView there.
Example:
containerView -> the view that contains your logo
logo -> the view you want vertically centered
logo.center.y = containerView.center.y
If the containerView is the screen, then
let screen = UIScreen.main.bounds
let height = screen.height
logo.center.y = screen.height / 2
I would suggest using extensions on UIView to make auto layout much easier. This seems like a lot of code to begin with for such a simple task but these convenience functions will make things a lot quicker in the long run for example:
public func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero){
translatesAutoresizingMaskIntoConstraints = false
//Set top, left, bottom and right constraints
if let top = top {
topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
}
//Set size contraints
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
Then you could simply call:
someUIView.anchor(anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: nil, padding: .init(top: 16, left: 16, bottom: 16, right: 16), size: .init(width: 80, height: 80))
Then to answer your question directly you could then add more extensions to do some stuff easily:
public func anchorCenterXToSuperview(constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
if let anchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
}
}
Finally you could then simply call anchorCenterXToSuperview() on any UIView to centre any object.
Don't forget to make sure you've added your view to a view hierarchy before attempting to layout your views otherwise you'll get errors.

In Swift, programmatically creating UIView and adding controls to it and using auto layout, causes the controls to appear on the view's parent

I am trying to write a simple composite component for iOS in Swift 3. It consists of a UILabel followed by an UITextField laid out horizontally followed by a line under them. But What happens is the UILabel disappears, UITextField appears on the parent view and line also disappears.
My design in sketch
What it actually looks like in the Storyboard
My component's constraints in the view controller
My intention was to use Auto Layout, anchor the label to top and leading anchors of the view, anchor the textfield to top of the view and trailing anchor of the label with a constant, so they would appear side by side.
I did do a lot of research on this, one site that looked pretty close to what I wanted was https://www.raywenderlich.com/125718/coding-auto-layout, and I think I am following more or less the same approach.
I am doing something obviously wrong, but can't figure out what. Any help is much appreciated, I have been at this for a few days now.
import UIKit
#IBDesignable
class OTextEdit: UIView {
#IBInspectable var LabelText: String = "Label"
#IBInspectable var SecureText: Bool = false
#IBInspectable var Color: UIColor = UIColor.black
#IBInspectable var Text: String = "" {
didSet {
edit.text = Text
}
}
fileprivate let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 35))
fileprivate let edit = UITextField(frame: CGRect(x: 210, y: 0, width: 200, height: 35))
fileprivate let line: UIView = UIView()
override var intrinsicContentSize: CGSize {
return CGSize(width: 300, height: 100)
}
func setup() {
label.text = LabelText
label.textColor = Color
label.font = UIFont(name: "Avenir Next Condensed", size: 24)
edit.font = UIFont(name: "Avenir Next Condensed", size: 24)
edit.borderStyle = .roundedRect
edit.isSecureTextEntry = SecureText
line.backgroundColor = UIColor.white
self.addSubview(label)
self.addSubview(edit)
self.addSubview(line)
}
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
setup()
setupConstaints()
}
func setupConstaints() {
label.translatesAutoresizingMaskIntoConstraints = false
edit.translatesAutoresizingMaskIntoConstraints = false
line.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -10).isActive = true
label.topAnchor.constraint(equalTo: topAnchor)
edit.leadingAnchor.constraint(equalTo: label.leadingAnchor, constant: 10).isActive = true
edit.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20).isActive = true
edit.topAnchor.constraint(equalTo: self.topAnchor)
line.heightAnchor.constraint(equalToConstant: 2.0).isActive = true
line.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10).isActive = true
line.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20).isActive = true
line.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
}
}
You haven't got a series of constraints top to bottom, so auto layout can't determine the content size of your object. You have tried to set this via the initrinsicContentSize but you shouldn't need to do this.
You also need to set a horizontal hugging priority for your label to let auto layout know that you want the text field to expand:
I removed your override of intrinsicContentSize and changed your constraints to:
Constrain the bottom of the label to the top of the line
Constrain the bottom of the line to the bottom of the superview
Constrain the baseline of the label to the baseline of the text field
Remove the constraint between the top of the text field and the superview
Set the horizontal hugging priority of the label.
func setupConstraints() {
label.translatesAutoresizingMaskIntoConstraints = false
edit.translatesAutoresizingMaskIntoConstraints = false
line.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.topAnchor.constraint(equalTo: topAnchor)
label.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -8).isActive = true
edit.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 10).isActive = true
edit.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20).isActive = true
edit.firstBaselineAnchor.constraint(equalTo: label.firstBaselineAnchor).isActive = true
line.heightAnchor.constraint(equalToConstant: 2.0).isActive = true
line.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10).isActive = true
line.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20).isActive = true
line.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
line.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
I think it is pretty close to what you are after.

Out of bounds UIView constraints programmatically

I have a cake of 3 UIView layers with programmatically constraints.
Constructed function for programmatically set up constraints:
func setupViewConstraints(item:UIView, leadingTo:NSLayoutXAxisAnchor, leadingCon:CGFloat,
trailingTo:NSLayoutXAxisAnchor, trailingCon:CGFloat, topTo:NSLayoutYAxisAnchor,
topCon:CGFloat, bottomTo:NSLayoutYAxisAnchor, bottomCon:CGFloat) {
item.translatesAutoresizingMaskIntoConstraints = false
item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
item.trailingAnchor.constraint(equalTo: trailingTo, constant: trailingCon).isActive = true
item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
item.bottomAnchor.constraint(equalTo: bottomTo, constant:bottomCon).isActive = true
}
The lowest base layer is lightGray.
view = UIView()
view.backgroundColor = .lightGray
The 2nd layer contains 2 UIView (red and blue) with constraints .
let red = UIView()
red.backgroundColor = .red
view.addSubview(red)
setupViewConstraints(item: red, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: -(view.frame.width)*0.2), topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: -(view.frame.width)*0.8)
let blue = UIView()
blue.backgroundColor = .blue
view.addSubview(blue)
setupViewConstraints(item: blue, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: -(view.frame.width)*0.2), topTo: red.bottomAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)
And on top i have yellow UIView layer, which overlaps all the lower layers.
let yellow = UIView()
yellow.backgroundColor = .yellow
view.addSubview(yellow)
setupViewConstraints(item: yellow, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: 0, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)
Also, i have UINavigationBar with UINavigationItem inside the yellow UIView.
//Add navigation item and buttons
naviItem = UINavigationItem()
naviItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem:.add, target:self, action:#selector(goToDestVC)), animated: true)
naviItem.setLeftBarButton(UIBarButtonItem(image: UIImage(named: "hamburger_slim_30"), style: .plain, target: self, action: #selector(hamburgerBtnPressed)), animated: true)
//Add navigation bar with transparent background
naviBar = UINavigationBar()
naviBar.setBackgroundImage(UIImage(), for: .default)
naviBar.shadowImage = UIImage()
naviBar.isTranslucent = true
// Assign the navigation item to the navigation bar
naviBar.items = [naviItem]
view.addSubview(naviBar)
setupViewConstraints(item: naviBar, leadingTo: yellow.leadingAnchor, leadingCon: 0, trailingTo: yellow.trailingAnchor, trailingCon: 0, topTo: yellow.topAnchor, topCon: 0, bottomTo: yellow.bottomAnchor, bottomCon: -(view.frame.height)*0.9))
And i have hamburgerBtnPressed function, which should shift the yellow layer to the right by 80% (I change the values of leading and trailing constants by 80%), but this does not work!!!
var hamburgerMenuIsVisible = false
#objc func hamburgerBtnPressed(_ sender: Any) {
if !hamburgerMenuIsVisible {
let menuWidth = (self.view.frame.width)*0.8
setupViewConstraints(item: layoutView, leadingTo: view.leadingAnchor, leadingCon: menuWidth, trailingTo: view.trailingAnchor, trailingCon: menuWidth, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)
hamburgerMenuIsVisible = true
} else {
setupViewConstraints(item: layoutView, leadingTo: view.leadingAnchor, leadingCon: 0, trailingTo: view.trailingAnchor, trailingCon: 0, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)
hamburgerMenuIsVisible = false
}
// layoutIfNeeded() lays out the subviews immediately and forces the layout before drawing
UIView.animate(withDuration: 0.2, delay:0.0, options: .curveEaseIn, animations: {
self.view.layoutIfNeeded()
}) { (animationComplete) in
print("Animation is complete!")
}
}
But if I change the values of leading and trailing constants to negative, everything will work, and the menu will shift to the left without any problems.
let menuWidth = -(self.view.frame.width)*0.8
Please explain.. what's the issue? Why the yellow UIView shifted to the left with negative values of constraints, and does not work with positive values of constraints? and gives an error:
Probably at least one of the constraints in the following list is one you don't want.
(
"<NSLayoutConstraint:0x6040002853c0 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing (active)>",
"<NSLayoutConstraint:0x604000092750 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing + 331.2 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x604000092750 UIView:0x7fa947c35850.trailing == UIView:0x7fa947e1d2d0.trailing + 331.2 (active)>
Update: I have choose Option 2:
Keep a reference to the constraint that you want to change and just adjust its constant. Need to call setNeedsLayout too before layoutIfNeeded.
Updated code:
var leadingC: NSLayoutConstraint!
var trailingC: NSLayoutConstraint!
var yellow: UIView!
loadView():
yellow = UIView()
yellow.backgroundColor = .yellow
view.addSubview(yellow)
//Set up leading and trailing constraints for handling yellow view shift
leadingC = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0)
trailingC = yellow.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
//Put leadingC.constant and trailingC.constant into the function
setupViewConstraints(item: yellow, leadingTo: view.leadingAnchor, leadingCon: leadingC!.constant, trailingTo: view.trailingAnchor, trailingCon: trailingC.constant, topTo: view.topAnchor, topCon: 0, bottomTo: view.bottomAnchor, bottomCon: 0)
Updated Hamburger function:
#objc func hamburgerBtnPressed(_ sender: Any) {
if !hamburgerMenuIsVisible {
let menuWidth = (self.view.frame.width)*0.8
leadingC!.constant = menuWidth
trailingC!.constant = menuWidth
print(leadingC.constant, trailingC.constant)
hamburgerMenuIsVisible = true
} else {
leadingC!.constant = 0
trailingC!.constant = 0
hamburgerMenuIsVisible = false
}
// layoutIfNeeded() lays out the subviews immediately and forces the layout before drawing
UIView.animate(withDuration: 0.2, delay:0.0, options: .curveEaseIn, animations: {
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}) { (animationComplete) in
print("Animation is complete!")
}
}
var hamburgerMenuIsVisible = false
I have no errors and "Animation complete!" was printed too, but nothing happens on the screen, no animation.
Firstly, it needs negative values because the constraints need to be set up in the correct direction. Change this and you can remove all of those negative constants:
item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
item.trailingAnchor.constraint(equalTo: trailingTo, constant: trailingCon).isActive = true
item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
item.bottomAnchor.constraint(equalTo: bottomTo, constant:bottomCon).isActive = true
to
item.leadingAnchor.constraint(equalTo: leadingTo, constant: leadingCon).isActive = true
trailingTo.constraint(equalTo: item.trailingAnchor, constant: trailingCon).isActive = true
item.topAnchor.constraint(equalTo: topTo, constant:topCon).isActive = true
bottomTo.constraint(equalTo: item.bottomAnchor, constant:bottomCon).isActive = true
Secondly, every time you call setupViewConstraints you are creating and activating another set of constraints.
Option 1:
Remove all constraints for the yellow view before setting them up again.
Option 2:
Keep a reference to the constraint that you want to change and just adjust its constant. You may need to call setNeedsLayout too before layoutIfNeeded.
Option 3:
Add 2 contraints. The initial leading constraint, and one with the width you desire. Change the priority of the first constraint to 999 (default is 1000) and toggle the isActive property of the other when you want to show/hide the menu.
let leading = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0)
leading.priority = UILayoutPriority(999)
leading.isActive = true
let otherConstraint = yellow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: (view.frame.width)*0.8)
otherConstraint.isActive = false // toggle this property to show/hide
Option 2 is probably going to be the best for performance. From the apple docs:
Setting the constant on an existing constraint performs much better
than removing the constraint and adding a new one that's exactly like
the old except that it has a different constant

How to find out the distance from the bottom safe area edge to the bottom of the screen?

I need to calculate the distance between the bottom anchor of the safe area and the bottom of the screen. Is there a way to do that in code given a view?
Try this one
if #available(iOS 11.0, *) {
let window = UIApplication.shared.keyWindow
let bottomPadding = window?.safeAreaInsets.bottom
}
You could try pinning a subview (clear , hidden or whatever) to the bottom of the safeAreaLayoutGuide and calculate the difference between the bottom of this view and your view controller's view in viewDidLayoutSubviews.
class ViewController: UIViewController {
let measuringView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
measuringView.backgroundColor = .magenta
measuringView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(measuringView)
let vConstraint = measuringView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
let heightConstraint = measuringView.heightAnchor.constraint(equalToConstant: 34)
var constraints = NSLayoutConstraint.constraints(withVisualFormat: "|[measuring]|", options: [], metrics: nil, views: ["measuring": measuringView])
constraints.append(vConstraint)
constraints.append(heightConstraint)
NSLayoutConstraint.activate(constraints)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let measuringBottom = measuringView.frame.origin.y + measuringView.frame.height
let viewBottom = view.bounds.height
let distance = abs(measuringBottom - viewBottom)
print("distance is \(distance) points")
}
}
To re-iterate previous answers. Pin a subview to the bottom of the view of a UIViewController. Then pin a second one to the view.safeAreaLayoutGuide.bottomAnchor anchor of the view. With both subviews pinning the top, leading and trailing anchors of the parent view. Then, I would assume in viewDidAppear, you could print out the difference between the two subview's frame.maxY values. This should give you the difference.
let viewA = UIView()
let viewB = UIView()
override func viewDidLoad() {
view.addSubview(viewA)
view.addSubview(viewB)
viewA.translateAutoResizingMasksIntoConstraints = false
viewB.translateAutoResizingMasksIntoConstraints = false
if #available(iOS 11.0, *) {
NSLayoutConstraint.activate([viewA.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
viewA.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
viewA.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
viewA.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
viewB.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
viewB.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
viewB.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
viewB.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)])
} else {
// Fallback on earlier versions
}
}
override func viewDidAppear() {
super.viewDidAppear()
print("Safe Distance Value is:\(viewA.frame.maxY - viewB.frame.maxY)")
}
As a reference for others, the value appears to be 34 on an iPhone X simulator
Here is a solution that works well on tableviews with adding a bottomView for buttons:
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
buttonsView.backgroundColor = .secondarySystemBackground
self.tableView.addSubview(buttonsView)
let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let bottomPadding = window?.safeAreaInsets.bottom ?? 0
buttonsView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: bottomPadding ).isActive = true
buttonsView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor).isActive = true
buttonsView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor).isActive = true
buttonsView.heightAnchor.constraint(equalToConstant: 88.0 + bottomPadding ).isActive = true

Getting nslayoutconstraints from UIView

after i have set up the constraints using anchor properties, such as this.
pageControl.anchorWithFixedHeight(nil, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, topConstant: 0, leadingConstant: 0, bottomConstant: 0, trailingConstant: 0, heightConstant: 30)
where anchorWithFixedHeight is just a helper function like the following:
func anchorWithFixedHeight(_ top: NSLayoutYAxisAnchor? = nil, leading: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, trailing: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leadingConstant: CGFloat = 0, bottomConstant: CGFloat = 0, trailingConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
anchors.append(topAnchor.constraint(equalTo: top, constant: topConstant))
}
if let leading = leading {
anchors.append(leadingAnchor.constraint(equalTo: leading, constant: leadingConstant))
}
if let bottom = bottom {
anchors.append(bottomAnchor.constraint(equalTo: bottom, constant: bottomConstant))
}
if let trailing = trailing {
anchors.append(trailingAnchor.constraint(equalTo: trailing, constant: trailingConstant))
}
anchors.append(heightAnchor.constraint(equalToConstant: heightConstant))
}
Now, let's say i wish to get the bottom anchor constraint to change its property, how can i achieve that?
Thanks in advance
Edit: The anchors here is an array of NSlayoutContraint. My current method works in such a way that i append all the needed constraints into this array, then get whichever constraint, for example bottom anchor constraint from this array.
However, i felt that the solution is not eloquent enough. Thus, was wondering if there is any other better approach.
Let's clean up the signature of your method first. Since the constants are useless without the corresponding anchors, let's pass them together in tuples. We should also name the function after what it returns, which is constraints, not anchors.
If you want access to the constraints by what they constrain, we can do that in a type-safe way by returning a dictionary whose key type is NSLayoutAttribute and whose value type is NSLayoutConstraint. Thus we will call this new method like this:
let root = UIView()
let view = UIView()
root.addSubview(view)
let constraints = view.helperConstraints(
leading: (root.leadingAnchor, 20),
bottom: (root.bottomAnchor, 10),
trailing: (root.trailingAnchor, 20),
heightConstant: 30)
// Activate all the new constraints.
NSLayoutConstraint.activate(Array(constraints.values))
// Change the constant of the bottom constraint.
constraints[.bottom]!.constant = 40
The implementation isn't that different from yours:
extension UIView {
func helperConstraints(
top: (NSLayoutYAxisAnchor, CGFloat)? = nil,
leading: (NSLayoutXAxisAnchor, CGFloat)? = nil,
bottom: (NSLayoutYAxisAnchor, CGFloat)? = nil,
trailing: (NSLayoutXAxisAnchor, CGFloat)? = nil,
heightConstant: CGFloat? = nil) -> [NSLayoutAttribute: NSLayoutConstraint] {
translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutAttribute: NSLayoutConstraint]()
if let (anchor, constant) = top {
constraints[.top] = topAnchor.constraint(equalTo: anchor, constant: constant)
}
if let (anchor, constant) = leading {
constraints[.leading] = leadingAnchor.constraint(equalTo: anchor, constant: constant)
}
if let (anchor, constant) = bottom {
constraints[.bottom] = bottomAnchor.constraint(equalTo: anchor, constant: constant)
}
if let (anchor, constant) = trailing {
constraints[.trailing] = trailingAnchor.constraint(equalTo: anchor, constant: constant)
}
if let constant = heightConstant {
constraints[.height] = heightAnchor.constraint(equalToConstant: constant)
}
return constraints
}
}

Resources