Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trailing and Leading constraints in Swift programmatically (NSLayoutConstraints)

I'm adding from a xib a view into my ViewController. Then I'm putting its constraints to actually fit it

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    ...
    ...
    view!.addSubview(gamePreview)
    gamePreview.translatesAutoresizingMaskIntoConstraints = false
    if #available(iOS 9.0, *) {
        // Pin the leading edge of myView to the margin's leading edge
        gamePreview.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
        //Pin the trailing edge of myView to the margin's trailing edge
        gamePreview.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true

    } else {
        // Fallback on earlier versions
        view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .TrailingMargin, relatedBy: .Equal, toItem: view, attribute: .TrailingMargin, multiplier: 1, constant: 0))

        view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .LeadingMargin, relatedBy: .Equal, toItem: view, attribute: .LeadingMargin, multiplier: 1, constant: 0))
    }
    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0))
    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute,multiplier: 1, constant: 131))
}

What i'm trying to do: To actually fit my view constraining it to top, leading, trailing to ViewController's view, and with a prefixed height. The view I'm adding to main view has its own transparent-background view, so no need of margin (the view is meant to be device's width size, so).

I've placed 2 couples of lines that would be supposed to be equal (in my attempts), with the if, because the first 2 lines in if are actually available in iOS9> only, while I'm attempting to do the same thing in the else statement, for every device (starting from iOS 8).

This is what I get:

iOS9+ at left, iOS8+ at right. Transparent background was colored red to show what happens (don't mind at different height in the images, they're equal height in app, instead look at added margins at left and right)

I also tried AutoLayoutDSL-Swift, but doesn't help, I'm not expert with it but every attempt made only things worse.

How can I write those constraints using classic NSLayoutConstraints methods, and how could I write all in a better way with a framework like AutoLayoutDSL or a fork of it? (or an alternative, though now mostly I'm concerned on official libs)

like image 620
BlackBox Avatar asked Sep 27 '16 20:09

BlackBox


People also ask

How do I give a constraint to button programmatically in Swift?

Add the button in the view, give it constraints and as you are using constraints, you can skip the button. frame and add widthAnchor and heightAnchor . At last activate them and keep translatesAutoresizingMaskIntoConstraints as false . Also, it will be better if you can add proper names.

What is leading and trailing in Swift?

In this article we will learn about the difference between leading and left, trailing and right constraints in Swift. In short, by setting the leading constraint we set the starting point of a view, while the trailing constraint sets the ending point.


4 Answers

You need to use the leading and trailing attributes, not the leadingMargin and trailingMargin attributes:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    ...
    ...
    view!.addSubview(gamePreview)
    gamePreview.translatesAutoresizingMaskIntoConstraints = false

    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1, constant: 0))            
    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1, constant: 0))

    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0))
    view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute,multiplier: 1, constant: 131))
}
like image 115
Paulw11 Avatar answered Oct 23 '22 14:10

Paulw11


Guessed it thanks to @Paulw11... this is the solution

view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1, constant: 0))     
view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1, constant: 0))        
view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: gamePreview, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute,multiplier: 1, constant: 131))

Also I managed to rewrite it as it follows using AutoLayoutDSL-Swift for anyone interested

view => gamePreview.trailing == view.trailing
     => gamePreview.leading == view.leading
     => gamePreview.height == 131
     => gamePreview.top == view.top + self.navigationController!.navigationBar.bounds.height + UIApplication.sharedApplication().statusBarFrame.size.height

The last line is a little long because view.top refers to the really view top and doesn't consider padding added by both statusBar and navigationBar heights. They could be replaced as constants, waiting if someone comes up with a more elegant solution.

like image 44
BlackBox Avatar answered Oct 23 '22 14:10

BlackBox


SWIFT 4 Update

stackView.translatesAutoresizingMaskIntoConstraints = false

stackView.addConstraint(NSLayoutConstraint(item: stackView, attribute: .trailing, relatedBy: .equal, toItem: stackView, attribute: .trailing, multiplier: 1, constant: 0))
stackView.addConstraint(NSLayoutConstraint(item: stackView, attribute: .leading, relatedBy: .equal, toItem: stackView, attribute: .leading, multiplier: 1, constant: 0))

stackView.addConstraint(NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: self.addLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0))
stackView.addConstraint(NSLayoutConstraint(item: stackView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,multiplier: 1, 
constant: 131))
like image 39
SWARTY Avatar answered Oct 23 '22 14:10

SWARTY


Why don't you use constraint activation? You could do this:

    var anchors = [NSLayoutConstraint]()
    anchors.append(view.topAnchor.constraint(equalTo: gamePreview.topAnchor, constant: 0))
    anchors.append(view.leadingAnchor.constraint(equalTo: gamePreview.leadingAnchor, constant: 0))
    anchors.append(view.trailingAnchor.constraint(equalTo: gamePreview.trailingAnchor, constant: 0))
    anchors.append(view.heightAnchor.constraint(equalTo: gamePreview.heightAnchor, multiplier: 1, constant: 131))
    NSLayoutConstraint.activate(anchors)

This is another. Hope it helps. You could swap view and gamePreview depending on the parent / child, or you could create a general helper extension for your constraints. I found one in a tutorial provided by a YouTuber Here and slightly modified it to accept leading/trailing, left/right constraints. You could also add safeLayoutGuide to the below as well, which makes it future-friendly. Something like this:

    func anchor(_ top: UIView? = nil, left: UIView? = nil, bottom: UIView? = nil, right: UIView? = nil, width: UIView? = nil, height: UIView? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0, isForLeading: Bool = false) -> [NSLayoutConstraint] {
    translatesAutoresizingMaskIntoConstraints = false

    var anchors = [NSLayoutConstraint]()
    if #available(iOS 11.0, *) {
        if let top = top {
            anchors.append(topAnchor.constraint(equalTo: top.safeAreaLayoutGuide.topAnchor, constant: topConstant))
        }

        if let left = left {
            if isForLeading  {
                anchors.append(leadingAnchor.constraint(equalTo: left.safeAreaLayoutGuide.leadingAnchor, constant: leftConstant))
            } else {
                anchors.append(leftAnchor.constraint(equalTo: left.safeAreaLayoutGuide.leftAnchor, constant: leftConstant))
            }
        }

        if let bottom = bottom {
            anchors.append(bottomAnchor.constraint(equalTo: bottom.safeAreaLayoutGuide.bottomAnchor, constant: -bottomConstant))
        }

        if let right = right {
            if isForLeading  {
                anchors.append(trailingAnchor.constraint(equalTo: right.safeAreaLayoutGuide.leadingAnchor, constant: rightConstant))
            } else {
                anchors.append(rightAnchor.constraint(equalTo: right.safeAreaLayoutGuide.rightAnchor, constant: -rightConstant))
            }
        }
        if let width = width {
            anchors.append(widthAnchor.constraint(equalTo: width.safeAreaLayoutGuide.widthAnchor, multiplier: 1, constant: widthConstant))
        } else if widthConstant > 0 {
            anchors.append(widthAnchor.constraint(equalToConstant: widthConstant))
        }

        if let height = height {
            anchors.append(heightAnchor.constraint(equalTo: height.safeAreaLayoutGuide.heightAnchor, multiplier: 1, constant: widthConstant))
        } else if heightConstant > 0 {
            anchors.append(widthAnchor.constraint(equalToConstant: heightConstant))
        }
        anchors.forEach({$0.isActive = true})
    } else {
        if let top = top {
            anchors.append(topAnchor.constraint(equalTo: top.topAnchor, constant: topConstant))
        }

        if let left = left {
            if isForLeading  {
                anchors.append(leadingAnchor.constraint(equalTo: left.leadingAnchor, constant: leftConstant))
            } else {
                anchors.append(leftAnchor.constraint(equalTo: left.leftAnchor, constant: leftConstant))
            }
        }

        if let bottom = bottom {
            anchors.append(bottomAnchor.constraint(equalTo: bottom.bottomAnchor, constant: -bottomConstant))
        }

        if let right = right {
            if isForLeading  {
                anchors.append(trailingAnchor.constraint(equalTo: right.leadingAnchor, constant: rightConstant))
            } else {
                anchors.append(rightAnchor.constraint(equalTo: right.rightAnchor, constant: -rightConstant))
            }
        }

        if let width = width {
            anchors.append(widthAnchor.constraint(equalTo: width.widthAnchor, multiplier: 1, constant: widthConstant))
        } else if widthConstant > 0 {
            anchors.append(widthAnchor.constraint(equalToConstant: widthConstant))
        }

        if let height = height {
            anchors.append(heightAnchor.constraint(equalTo: height.heightAnchor, multiplier: 1, constant: widthConstant))
        } else if heightConstant > 0 {
            anchors.append(widthAnchor.constraint(equalToConstant: heightConstant))
        }
        anchors.forEach({$0.isActive = true})
    }

    return anchors
}

And then call it like this:

        _ = view.anchor(gamePreview, left: gamePreview, right: gamePreview, height: gamePreview, heightConstant: 131, isForLeading: true)
like image 2
Septronic Avatar answered Oct 23 '22 14:10

Septronic