noblerare
noblerare

Reputation: 11893

Swift 3 - Create title bar constraints programmatically

I am using Swift 3, iOS 10, XCode 8.2.

In my code, I need to create a UIViewController programmatically and hence, specify its layout and content programmatically as well.

@IBAction func testViewController() {

    let detailViewController = UIViewController()
    detailViewController.view.backgroundColor = UIColor.white

    let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints =  false
        label.backgroundColor = UIColor.blue
        label.text = "Scan Results"
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 18)
        label.textColor = UIColor.white
        return label
    }()

    let titleConstraints: [NSLayoutConstraint] = [
        NSLayoutConstraint(item: titleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: titleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
    ]

    detailViewController.view.addSubview(titleLabel)

    detailViewController.view.addConstraints(titleConstraints)

    self.navigationController?.pushViewController(detailViewController, animated: true)
}

In the vertical view (ignore all the other junk; just focus on the blue title bar):

enter image description here

But in the horizontal view:

enter image description here

What is the correct constraint to set so that it takes up the entire width of the bar and there isn't that extra space from the top since the status bar disappears when horizontal?

EDIT

After making @thexande suggestions, I do get an error:

[LayoutConstraints] The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x608000098100 UILabel:0x7fe35b60edc0'Scan Results'.left == UIView:0x7fe35b405c20.left (inactive)> When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView(UIConstraintBasedLayout) _viewHierarchyUnpreparedForConstraint:] to debug. 2017-02-24 21:01:59.807 EOB-Reader[78109:10751346] * Assertion failure in -[UIView _layoutEngine_didAddLayoutConstraint:roundingAdjustment:mutuallyExclusiveConstraints:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/NSLayoutConstraint_UIKitAdditions.m:649 2017-02-24 21:01:59.951 EOB-Reader[78109:10751346] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Impossible to set up layout with view hierarchy unprepared for constraint.'

I've also updated my code in the original post.

Upvotes: 1

Views: 4500

Answers (1)

thexande
thexande

Reputation: 1750

The reason this is happening is because you are using frames. You calculated the frame based on the width of the screen. You do not need frames, you can do this all using auto layout. Instead, you should use constraints to pin your label to it's super view bounds, and give it a static height. for example:

lazy var titleConstraints: [NSLayoutConstraint] = [
    NSLayoutConstraint(item: self.titleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: self.titleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: self.titleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
    NSLayoutConstraint(item: self.titleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
]

then, in viewDidLoad()

self.view.addConstraints(titleConstraints)

You could simplify your label declaration like so. dont forget the auto resizing mask flag to get constraints to work correctly:

let titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints =  false
    label.backgroundColor = UIColor.blue
    label.text = "Scan Results"
    label.textAlignment = .center
    label.textColor = UIColor.white
    return label
}()

Finally, you are doing strange math to get the top of your view controller to abut the bottom of your nav bar controller. Remove all that garbage and put the following in viewDidLoad() to get the top of your view controller right against the bottom of your UINavigationBar:

self.edgesForExtendedLayout = []

UPDATES:

The problem here is you are appending views and constraints into a View Controller which has not allocated yet.

The reason we append sub views and constraints within viewDidLoad() is because we cannot add subviews and constraints before the view....did....load into memory. Otherwise, it's not there, and you get the error above. Consider breaking out your detailViewController into a class declaration, like so:

class detailViewController: UIViewController {
let eobTitleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints =  false
    label.backgroundColor = UIColor.blue
    label.text = "Scan Results"
    label.textAlignment = .center
    label.font = UIFont.boldSystemFont(ofSize: 18)
    label.textColor = UIColor.white
    return label
}()

    lazy var eobTitleConstraints: [NSLayoutConstraint] = [
       NSLayoutConstraint(item: self.eobTitleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: self.eobTitleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: self.eobTitleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
        NSLayoutConstraint(item: self.eobTitleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
    ]

    override func viewDidLoad() {
        self.view.addSubview(eobTitleLabel)
        self.view.addConstraints(self.eobTitleConstraints)
    }
}

Also, not to come off as offensive, but your code is kind of a mess. Things you should avoid in the future:

  1. adding constraints to a label which does not exist. ( rename the label of fix the constraints)
  2. you are declaring vars in a outlet method. dont do this, declare methods and properties at the class level.
  3. Read about OOP and how it is implemented in swift. This will help you understand the methods and patterns to complete your task :)

Upvotes: 4

Related Questions