Adding constraints programatically to custom components - swift

I'm developing custom components in Swift by inheriting the default components. Below there is a piece of my code:
class DirectionButton: UIButton {
var backgroundImage: UIImageView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
styleComponent()
}
override init(frame: CGRect) {
super.init(frame: frame)
styleComponent()
}
func styleComponent() {
backgroundImage = UIImageView(image: UIImage(named: "seta-proximo"))
backgroundImage.contentMode = .ScaleAspectFit
self.setNeedsLayout()
self.layoutIfNeeded()
self.addSubview(backgroundImage)
let imageConstraints = NSLayoutConstraint(item: self, attribute: .TrailingMargin, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 10)
backgroundImage.addConstraint(imageConstraints)
}
}
The backgroundImage variable needs to be 10 points from the right part of the button, that's why I need the constraints.
After running I got an exception like that: The view hierarchy is not prepared for the constraint.
How can I add constraints correctly?

Reading the documentation, I followed the tip to use layout anchors as #AjayBeniwal suggested. The code I needed was the following (after I add the subview to the button):
backgroundImage.trailingAnchor.constraintEqualToAnchor(self.layoutMarginsGuide.trailingAnchor, constant: -10).active = true
backgroundImage.bottomAnchor.constraintEqualToAnchor(self.layoutMarginsGuide.bottomAnchor).active = true
backgroundImage.centerYAnchor.constraintEqualToAnchor(self.layoutMarginsGuide.centerYAnchor).active = true
This way, the image is vertically centralized to the button an 20 points far from the right edge.

Edited
first do this :
backgroundImage.translatesAutoresizingMaskIntoConstraints = false
Add constraint to self not backgroundImage
You are adding constraint from self to self (incorrect) add from backroundImage to self
Constraints are incomplete. You are adding only one trailing constraint.
Overall solution is this:
func styleComponent() {
// Change clear.png with your own image name.
backgroundImage = UIImageView(image: UIImage(named: "clear.png"))
backgroundImage.contentMode = .ScaleAspectFit
self.addSubview(backgroundImage)
backgroundImage.translatesAutoresizingMaskIntoConstraints = false
let imageConstraints = NSLayoutConstraint(item: backgroundImage, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: -10)
self.addConstraint(imageConstraints)
let topConstraints = NSLayoutConstraint(item: backgroundImage, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 10)
self.addConstraint(topConstraints)
let bottomConstraints = NSLayoutConstraint(item: backgroundImage, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: -10)
self.addConstraint(bottomConstraints)
let leadingConstraints = NSLayoutConstraint(item: backgroundImage, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 10)
self.addConstraint(leadingConstraints)
}
The resultant output is this. You can modifiy the constants values to do according to your own will
In this case, The green is a Button and Red one is background image.
Try this and let me know if you are still getting any warning or complications.

//Give Constrain Outlet to ImageView
func setConstrain()
{
self.imageConstrain.constant = -10
self.topConstraints.constant = 10
self.bottomConstraints.constant = -10
self.leadingConstraints.constant = 10
self.layoutIfNeeded()
}

Related

Adding constraints in awakeFromNib() method

I have two custom views in my project which needs to be zoomed in the exact same proportion. So I decided to use UIScrollView for that and it fits perfectly.
I decided to develop a very simple class inherited from UIScrollView and inside of it I initialized all views structure. That way I'm avoiding any construction steps in the NIB file and my class can be used just by adding one view. But I faced an issue already on the stage of adding contentView.
Here's my class:
final class PlayerContentView: UIScrollView {
fileprivate var contentView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .clear
setupScrollProperties()
setupContentView()
}
private func setupScrollProperties()
{
self.translatesAutoresizingMaskIntoConstraints = false
self.minimumZoomScale = 1.0
self.maximumZoomScale = 2.0
self.contentSize = frame.size
self.delegate = self
}
private func setupContentView()
{
contentView = UIView(frame: self.frame)
contentView.backgroundColor = .red
self.addSubview(contentView)
CommonSwiftUtility.setSideConstraints(superview: self, view: contentView)
CommonSwiftUtility.setSizeConstraints(superview: self, view: contentView)
}
func requireToFail(_ gestureRecognizer: UIPanGestureRecognizer) {
self.panGestureRecognizer.require(toFail: gestureRecognizer)
} }
And here're methods for adding constraints:
static func setSideConstraints(superview: UIView, view: UIView) {
let topConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .top,
relatedBy: .equal,
toItem: superview,
attribute: .top,
multiplier: 1.0, constant: 0.0)
let bottomConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .bottom,
relatedBy: .equal,
toItem: superview,
attribute: .bottom,
multiplier: 1.0, constant: 0.0)
let leadingConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .leading,
relatedBy: .equal,
toItem: superview,
attribute: .leading,
multiplier: 1.0, constant: 0.0)
let trailingConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .trailing,
relatedBy: .equal,
toItem: superview,
attribute: .trailing,
multiplier: 1.0, constant: 0.0)
superview.addConstraint(topConstraint)
superview.addConstraint(bottomConstraint)
superview.addConstraint(leadingConstraint)
superview.addConstraint(trailingConstraint)
}
static func setSizeConstraints(superview: UIView, view: UIView)
{
let wConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .width,
relatedBy: .equal,
toItem: superview,
attribute: .width,
multiplier: 1.0, constant: 0.0)
let hConstraint: NSLayoutConstraint = NSLayoutConstraint(item: view,
attribute: .height,
relatedBy: .equal,
toItem: superview,
attribute: .height,
multiplier: 1.0, constant: 0.0)
superview.addConstraint(wConstraint)
superview.addConstraint(hConstraint)
}
As you can see, I painted my contentView in red color in order to define it on the screen. After showing my PlayerContentView I get this:
PlayerContentView is stretched on the full screen so I'm expecting contentView to be full-size, but clearly it's not. Could someone please refer me to the solution of that issue? Thanks in advance!
Can you set
contentView.translatesAutoresizingMaskIntoConstraints = false

Programmatic Constraints not working as expected

I've spent hours trying to hunt down what is preventing my constraints layout from working. I have a view called ABSegment which simply holds a UILabel which should be centered and have equal height and width as its superview.
I'll include the entire class definition of this UIView subview to show what I've done.
import UIKit
class ABSegment: UIView {
let titleLabel = UILabel()
init(withTitle title: String) {
titleLabel.text = title
titleLabel.textAlignment = .center
super.init(frame: CGRect.zero)
translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
backgroundColor = UIColor.blue
}
override func layoutSubviews() {
super.layoutSubviews()
logFrames()
}
func logFrames() {
print("self.frame is \(frame)")
print("titleLabel.frame is \(titleLabel.frame)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
logFrames()
titleLabel.removeConstraints(titleLabel.constraints)
let centerX = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
let height = NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY, width, height])
super.updateConstraints()
}
}
One natural thing to suspect is that I'm not initializing this view with initWithFrame. Rather, I'm deferring the frame setting until later in the code where these views are constructed. So code that constructs these don't set the frame. The frame may be set in Storyboard or like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let frame = CGRect(x: view.bounds.origin.x + 150, y: view.bounds.origin.y + 200, width: 100, height: 100)
segment.frame = frame
segment.layoutSubviews()
}
My understanding is that since I'm calling segment.layoutSubviews(), the ABSegment View should have a change to apply the previously activated constraints with a final frame.
There are so many different settings and things to get right in the correct order with no feedback other than not seeing the Label appear at all.
The main problems I see with your code:
You are adding and removing constraints every time updateConstraints is called. You only need to set up the constraints once when you create your view.
You are setting translatesAutoresizingMaskIntoConstraints = false on ABSegment itself. Don't do this. This tells Auto Layout that you will be specifying the size and location of ABSegment using constraints, and you clearly are using a frame for this purpose.
Here is the refactored code:
class ABSegment: UIView {
let titleLabel = UILabel()
// Computed property to allow title to be changed
var title: String {
set {
titleLabel.text = newValue
}
get {
return titleLabel.text ?? ""
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLabel(title: "")
}
convenience init(title: String) {
self.init(frame: CGRect.zero)
self.title = title
}
// This init is called if your view is
// set up in the Storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLabel(title: "")
}
func setupLabel(title: String) {
titleLabel.text = title
titleLabel.textAlignment = .center
addSubview(titleLabel)
backgroundColor = UIColor.blue
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let centerX = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
let height = NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY, width, height])
}
override func layoutSubviews() {
super.layoutSubviews()
logFrames()
}
func logFrames() {
print("self.frame is \(frame)")
print("titleLabel.frame is \(titleLabel.frame)")
}
override func updateConstraints() {
super.updateConstraints()
logFrames()
}
}
Notes:
I moved all of the setup of the label into a function called setupLabel. This allows it to be called by both initializers.
I added a computed property called title to ABSegment which allows you to change the title at any time with mysegment.title = "new title".
I turned init(withSegment:) into a convenience init. It calls the standard init(frame:) and then sets the title. It is not a common practice to use withProperty so I changed it. You'd create an ABSegment with var segment = ABSegment(title: "some title").
I had required init?(coder aDecoder: NSCoder) call setupLabel so that ABSegment can be used with views added in the Storyboard.
The overrides of layoutSubviews and updateConstraints were left in to log the frames. That is all that they do now.

Swift3 setting constraints relative to parent view of parent view

I have 3 classes, one is a a normal UIViewController in in which I have this code:
let CC0 = CustomUIView0()
self.addSubview(CC0)
the CustomUIView00 is a UIView class containing this code:
let CC1 = CustomUIView1()
self.addSubview(CC1)
Now for CC1 I would like to activate a constraint like this
NSLayoutConstraint.activate([
NSLayoutConstraint(item: CC1, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: ("Parent view of CC0"), attribute: NSLayoutAttribute.top, multiplier: 1, constant: 0)
])
What I tried to do is to use the UIScreen.main in the toItem place but this throws an error saying that the item needs to be an instance of UIView for this to work.
So I would essentially like to create a constraint relative to the parent of the paren view, how could I do this.
Update:
UIViewController:
let CC0 = UICustomUIView0()
self.view.insertSubview(self.CC0, at: 0)
UICustomUIView0:
init {
super.init(frame: UIScreen.main.bounds);
let CC1 = UICustomUIView1()
guard let CC1Parent = self.CC1.superview, let CC0Parent = CC1Parent.superview else {
print("Problem")
return
}
self.view.addSubview(CC1)
NSLayoutConstraint.activate([
NSLayoutConstraint(item: CC1, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: CC0Parent, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 0)
])
}
This throws the problem print
Try below:
// guard below ensures you have the parent AND grandparent views
guard let cc1Parent = cc1.superview, let cc0Parent = cc1Parent.superview else {
// problem with accessing parent views
return
}
// now just set your constraints as per below
NSLayoutConstraint(item: cc1, attribute: .top, relatedBy: .equal, toItem: cc1Parent, attribute: .top, multiplier: 1, constant: 0)
EDIT: For UIView put code in awakeFromNib()
override func awakeFromNib() {
super.awakeFromNib()
// Set layout constraints here
}

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):
But in the horizontal view:
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.
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:
adding constraints to a label which does not exist. ( rename the label of fix the constraints)
you are declaring vars in a outlet method. dont do this, declare methods and properties at the class level.
Read about OOP and how it is implemented in swift. This will help you understand the methods and patterns to complete your task :)

Autolayout constraints set in code not appearing in interface builder

I have a custom view that is set in interface builder with top, leading, trailing, height constraints.
In my Custom view i have a title and a button.
Im trying to add to the title a bottom and centerY constraints.
and to the button width, height, bottom, leading constraints.
When i add any constraint i get an warning in interface builder:
Expected: width=600, height=68.
Actual: width=0, height=0
When i run the code everything works, but i cant see anything in interface builder.
code:
#IBDesignable
class UIHeader: UIView {
var delegate: HeaderDelegate?
private lazy var titleLable: UILabel = {
let lbl = UILabel()
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.font = UIFont(name: "Lato-Light", size: 16)
lbl.text = "Title"
return lbl
}()
private lazy var backButton: UIButton = {
let btn = UIButton()
btn.tintColor = UIColor.lightGrayColor()
btn.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "prev")
if let image = image {
btn.setImage(image.imageWithRenderingMode(.AlwaysTemplate), forState: .Normal)
}
btn.addTarget(self, action: #selector(UIHeader.OnBackButtonClickLister(_:)), forControlEvents: .TouchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
}
extension UIHeader {
#IBInspectable
var backButtonImage: UIImage? {
get {
return backButton.imageForState(.Normal)
}
set (newImage) {
backButton.setImage(newImage?.imageWithRenderingMode(.AlwaysTemplate), forState: .Normal)
}
}
#IBInspectable
var title: String? {
get {
return titleLable.text
}
set (newTitle) {
titleLable.text = newTitle
}
}
}
extension UIHeader {
private func setupView() {
backgroundColor = UIColor.whiteColor()
translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLable)
addSubview(backButton)
//add shadow
layer.shadowColor = UIColor(white: 115/255, alpha: 1.0).CGColor
layer.shadowOpacity = 0.5
layer.shadowRadius = 8
layer.shadowOffset = CGSizeMake(0, -1)
NSLayoutConstraint.activateConstraints([
//Title//
//center x
NSLayoutConstraint(item: titleLable, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1.0, constant: 0),
//bottom
NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, toItem: titleLable, attribute: .Bottom, multiplier: 1, constant: 12),
//button//
//bottom
NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, toItem: backButton, attribute: .Bottom, multiplier: 1, constant: 4),
//leading
NSLayoutConstraint(item: backButton, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1, constant: 0),
//width
NSLayoutConstraint(item: backButton, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .Width, multiplier: 1, constant: 40),
//height
NSLayoutConstraint(item: backButton, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .Height, multiplier: 1, constant: 40)
])
}
}
I also tried to add the constraints with:
addConstraint(NSLayoutConstraint)
cant figure out what is the problem.
Thanks
I removed
translatesAutoresizingMaskIntoConstraints = false
and everything works great.