NSAutoresizingMaskLayoutConstraint errors in an AutoLayout-based application - ios

Due to the sheer amount of AutoLayout-related questions I'm not entirely sure I am asking a new question, but I just can't get this to work. I'm trying to animate a view using AutoLayout, but I'm already failing at just translating it. I have worked with AL before using storyboards, this is my first try at using them programmatically.
So, to the problem: Starting off with a newly created master-detail iOS application, I deleted everything from the storyboard, inserted a new view controller and set its view's class to BaseView. The code for base view is as follows:
class BaseView: UIView {
#IBOutlet var secondaryView: UIView! = nil
override func awakeFromNib() {
setTranslatesAutoresizingMaskIntoConstraints(false)
backgroundColor = UIColor.redColor()
secondaryView = UIView(frame: frame)
secondaryView.backgroundColor = UIColor.blueColor()
addSubview(secondaryView)
let views = ["secondaryView": secondaryView, "self": self]
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(100)-[secondaryView]", options: NSLayoutFormatOptions.AlignAllLeading, metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[secondaryView(==self)]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[secondaryView(==self)]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
}
}
I also tried setting translatesAutoresizingMaskIntoConstraints to false via the storyboard's user defined runtime attributes, to no avail. What I would expect is to see a red square (the base view) on the left side of the simulator, and the rest of the simulator screen filled with the color blue. Instead everything is blue, and the log gives me the following errors:
2014-08-20 22:19:00.392 AutoLayoutAnimationTest[5585:473139] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7fa8c94b5f50 H:|-(100)-[UIView:0x7fa8c94b2540] (Names: '|':AutoLayoutAnimationTest.BaseView:0x7fa8c94afa80 )>",
"<NSAutoresizingMaskLayoutConstraint:0x7fa8c94bdfc0 h=--& v=--& UIView:0x7fa8c94b2540.midX == + 300>",
"<NSAutoresizingMaskLayoutConstraint:0x7fa8c94be1d0 h=--& v=--& H:[UIView:0x7fa8c94b2540(600)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fa8c94b5f50 H:|-(100)-[UIView:0x7fa8c94b2540] (Names: '|':AutoLayoutAnimationTest.BaseView:0x7fa8c94afa80 )>
I have no idea where these NSAutoresizingMaskLayoutConstraints come from, but from my understanding they are preventing the secondaryView from moving. What am I doing wrong?

Updated for Swift 2
By making the presumption that secondaryView doesn't already exist in the Storyboard and is just a property without any IBOutlet, I was able to make the following code work without any debugger complaint:
class BaseView: UIView {
//#IBOutlet var secondaryView: UIView! = nil
var secondaryView: UIView!
override func awakeFromNib() {
backgroundColor = UIColor.redColor()
secondaryView = UIView()
secondaryView.translatesAutoresizingMaskIntoConstraints = false // Swift 2
// secondaryView.setTranslatesAutoresizingMaskIntoConstraints(false) // Swift 1.2
secondaryView.backgroundColor = UIColor.blueColor()
addSubview(secondaryView)
let views = ["secondaryView": secondaryView, "self": self]
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(100)-[secondaryView]", options: NSLayoutFormatOptions.AlignAllLeading, metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[secondaryView(==self)]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[secondaryView(==self)]", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
}
}
Therefore, I think that the following code is (a much concise) equivalent of the previous code:
class BaseView: UIView {
var secondaryView: UIView!
override func awakeFromNib() {
backgroundColor = UIColor.redColor()
secondaryView = UIView()
secondaryView.translatesAutoresizingMaskIntoConstraints = false // Swift 2
// secondaryView.setTranslatesAutoresizingMaskIntoConstraints(false) // Swift 1.2
secondaryView.backgroundColor = UIColor.blueColor()
addSubview(secondaryView)
let views = ["secondaryView": secondaryView]
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(100)-[secondaryView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[secondaryView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
}
}

Related

Auto Layout Visual Format Language

I'm using this visual format layout "V:[v0]-16-|" in:
self.view.addConstraintsWithFormat("V:[v0]-16-|", views: imageView)
and the helper method is the following:
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: viewsDictionary))
}
but what I want in reality is something like "V:[v0]+16+|", I want the right constraint to go beyond the UIScreen. How can I achieve this?
You can use a negative margin to position the view past the edge of the parent. Just put the constant in brackets like this:
self.view.addConstraintsWithFormat("V:[v0]-(-16)-|", views: imageView)

Animate Visual Format Language Constraint

I am trying to animate a button from the left side of a container to the right side. The position of this button is defined by the Visual Format Language. (The project is 100% code, no Storyboard issues possible)
These are my lines for the left side (working):
messageInputContainerView.addSubview(moreButton)
messageInputContainerView.addConstraintsWithFormat(format: "H:|-3-[v0(35)]-8-[v1][v2(60)]|", views: moreButton, ..., ...)
messageInputContainerView.addConstraintsWithFormat(format: "V:|[v0]|", views: moreButton)
These for the right side + animation (not working):
//I have read that I first have to remove the old constraints in order to apply new ones
moreButton.removeConstraints(moreButton.constraints)
//The only difference here is that the pipe ('|') is on the right side and that I don't care about the two other objects in the container
messageInputContainerView.addConstraintsWithFormat(format: "H:[v0(35)]-3-|", views: moreButton)
messageInputContainerView.addConstraintsWithFormat(format: "V:|[v0]|", views: moreButton)
...
//These lines should be fine, but I include them nontheless
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut, animations: {
self.moreButton.setTitle("⤇", for: .normal)
self.messageInputContainerView.layoutIfNeeded()
})
If someone is wondering, the 'addConstraintsWithFormat' function looks like this:
extension UIView {
func addConstraintsWithFormat(format: String, views: UIView...){
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated(){
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
I am new to VFL in general, therefore I would really appreciate a helpful answer.
Here is an image of the container and the moreButton. (both at the very bottom)
Your problem is likely that your constraints are not added to the correct UIViews.
You don't have to worry about adding constraints to views if you activate the constraints instead by calling NSLayoutConstraint(activate:) with the VFL constraints:
extension UIView {
func addConstraintsWithFormat(format: String, views: UIView...){
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated(){
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
And then there is this line:
//I have read that I first have to remove the old constraints in order to apply new ones
moreButton.removeConstraints(moreButton.constraints)
Again, you should be deactivating constraints (setting their isActive property to false) instead of removing constraints. The problem with removing constraints like this is that some of the constraints on moreButton are stored in the constraints array of moreButton's superview.
Keep the constraints you want to deactivate in the future in an array in your VC and then pass the constraints to NSLayoutConstraint.deactivate() or set the isActive property to false for those constraints you wish to deactivate.
Update:
Your button isn't animating correctly. That is because it is still contrained to the textField. To animate your moreButton, you need to free it of other constraints that are restricting its position.
First, I would change addConstraintsWithFormat(format:views:) to return the constraints it created as [NSLayoutConstraint]. This will allow you to deactivate them later.
extension UIView {
func addConstraintsWithFormat(format: String, views: UIView...) -> [NSLayoutConstraint] {
var viewsDictionary = [String: UIView]()
let constraints: [NSLayoutConstraint]
for (index, view) in views.enumerated(){
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
constraints = NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary)
NSLayoutConstraint.activate(constraints)
return constraints
}
Add a property to your viewController:
var moreButtonConstraints = [NSLayoutConstraint]()
Then break up the format "H:|-3-[v0(35)]-8-[v1][v2(60)]|" into "H:|-3-[v0(35)]" and "H:|-46-[v1][v2(60)]|" and activate them separately. That will separate the constraints for your moreButton from the constraints for the other views, allowing you to move the moreButton without affecting the other views.
messageInputContainerView.addSubview(moreButton)
// save these constraints in a property to be deactivated later
moreButtonConstraints = messageInputContainerView.addConstraintsWithFormat(format: "H:|-3-[v0(35)]", views: moreButton)
// we don't need to keep these constraints, so assign them to _
_ = messageInputContainerView.addConstraintsWithFormat(format: "H:|-46-[v1][v2(60)]|", views: ..., ...)
_ = messageInputContainerView.addConstraintsWithFormat(format: "V:|[v0]|", views: moreButton)
Then, when it is time to animate:
// deactivate the old constraints
NSLayoutConstraint.deactivate(moreButtonConstraints)
// add constraints to move the moreButton to the right side
_ = messageInputContainerView.addConstraintsWithFormat(format: "H:[v0(35)]-3-|", views: moreButton)

Cannot find superview while adding constrains programmatically?

Referring this question Making a custom UIView subview that fills its superview while trying to add constraints programmatically.
I am using the code below to add two constraints programmticly; however, I get an error states that "Unable to parse constraint format" because the related view doesn't have a superview.
private func setupDownView(view: UIView) {
let downView = try? DownView(frame: view.bounds, markdownString: "")
downView!.translatesAutoresizingMaskIntoConstraints = false
let viewsDict = ["view": downView!]
downView!.addConstraint(NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict)[0])
downView!.addConstraint(NSLayoutConstraint.constraints(
withVisualFormat: "H:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict)[0])
view.addSubview(downView!);
}
Does the view auto become the superview once it has been added to its subview?
I have tried to add the view before setting up the constrains by addSubview before addConstraint and I get the error "The view hierarchy is not prepared for the constraint".
setupDownView is called in the following two places,
#IBOutlet weak var cardTags: UIView! {didSet { setupDownView(view: cardTags)}}
#IBOutlet weak var cardContent: UIView! {
didSet {
setupDownView(view: cardContent)
let tap = UILongPressGestureRecognizer(
target: self,
action: #selector(QuizViewController.tapDownHandler)
)
tap.minimumPressDuration = 0
cardContent.addGestureRecognizer(tap)
}
}
Constraints need to be added after view is in the hierarchy. So addSubview call must be before addConstraint calls.
Also ensure that addConstraint is called on it's superview, not downView, and that all constraints returned from constraintsWithVisualFormat: are added, not only the first one.
private func setupDownView(view: UIView) {
let downView = try? DownView(frame: view.bounds, markdownString: "")
downView!.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(downView!);
let viewsDict = ["view": downView!]
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict))
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict))
view.layoutIfNeeded()
}
As of iOS 8, you don't have to figure would which view to add constraints to, just activate them. For individual constraints, just set .isActive to true. To activate an entire array of constraints, use the NSLayoutConstraint.activate() convenience method.
For your code, instead of doing:
view.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict))
You'd use:
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[view]-0-|",
options: [],
metrics: nil,
views: viewsDict))
This works because the constraints already know which view or views they refer to, and Cocoa or Cocoa Touch can figure out the appropriate view in the view hierarchy to add the constraints to, which can be tricky depending on how the views are related (eg. siblings, parent/child, other).
The same caveats apply here. You need to make sure the views are in the view hierarchy before activating the constraints, or you will get errors. So, do .addSubview before activating the constraints.

How to set constraints for a reusable UIView in Swift with auto layout?

I'm trying to build a reusable UIView whose width should equal to its superview in Swift.
Since the size of its superview varies, I think I have to set constraints for it with auto layout.
But I can't figure out how to do it programmatically in Swift.
Here is the code for the reusable subview:
import UIKit
class bottomMenu: UIView {
#IBOutlet var bottomMenu: UIView!
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
NSBundle.mainBundle().loadNibNamed("bottomMenu", owner: self, options: nil)
bottomMenu.backgroundColor = UIColor.redColor()
//How to make the width of the bottom Menu equal to its superview?
self.addSubview(self.bottomMenu)
}
}
Can anyone show me how to make it?
Thanks
You can override didMoveToSuperview() method in your UIView subclass and add the constraints there:
override func didMoveToSuperview() {
super.didMoveToSuperview()
backgroundColor = UIColor.redColor()
let views = ["view" : self];
self.setTranslatesAutoresizingMaskIntoConstraints(false)
self.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[view]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
self.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
}
Add constraints for element inside. For each element add constraint for top, bottom, left and right. If you have any images that needs to be same size add width and height as well. If you can post the screenshot of the UIView I will add more information and will be able to be more helpful.
Also take a look at http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1 if you are new to autolayout.
The following code gives the loaded view the same height and width as the super view. (not sure what you wanted for height)
bottomMenu.setTranslatesAutoresizingMaskIntoConstraints(false)
//Views to add constraints to
let views = Dictionary(dictionaryLiteral: ("bottomMenu",bottomMenu))
// Horizontal constraints
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[bottomMenu]|", options: nil, metrics: nil, views: views)
self.addConstraints(horizontalConstraints)
// Vertical constraints
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[bottomMenu]|", options: nil, metrics: nil, views: views)
self.addConstraints(verticalConstraints)

Result with auto layout in a UITableView different when starting the app and once the app is running

I have a UITableView that I created in the Interface Builder. Inside are dynamic cells. I added the constraints like this (this method is called in tableView:cellForRowAtIndexPath) :
#IBOutlet weak var rowView: UIView!
#IBOutlet weak var flagImageView: UIImageView!
#IBOutlet weak var computedRateLabel: UILabel!
#IBOutlet weak var historyButton: UIButton!
func buildConstraintsFor() {
let viewsDictionary = ["rowView": rowView, "flagImageView": flagImageView, "view1": computedRateLabel, "historyButton": historyButton]
rowView.layer.borderWidth = 1
flagImageView.layer.borderWidth = 1
computedRateLabel.layer.borderWidth = 1
historyButton.layer.borderWidth = 1
rowView.layer.borderColor = UIColor.blackColor().CGColor
flagImageView.layer.borderColor = UIColor.purpleColor().CGColor
computedRateLabel.layer.borderColor = UIColor.orangeColor().CGColor
historyButton.layer.borderColor = UIColor.greenColor().CGColor
// remove constraints
rowView.removeConstraints(rowView.constraints())
historyButton.removeConstraints(historyButton.constraints())
// add constraints
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[flagImageView]-[view1]-[historyButton]-|", options: nil, metrics: nil, views: viewsDictionary))
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-1-[flagImageView]-1-|", options: nil, metrics: nil, views: viewsDictionary))
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-1-[view1]-1-|", options: nil, metrics: nil, views: viewsDictionary))
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-1-[historyButton]-1-|", options: nil, metrics: nil, views: viewsDictionary))
// size
historyButton.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[historyButton(==25)]", options: nil, metrics: nil, views: viewsDictionary))
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[rowView(<=68)]", options: nil, metrics: nil, views: viewsDictionary))
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[rowView(>=67)]", options: nil, metrics: nil, views: viewsDictionary))
flagImageView.addConstraint(NSLayoutConstraint(item: flagImageView, attribute: .Width, relatedBy: .Equal, toItem: flagImageView, attribute: .Height, multiplier: 1, constant: 0))
}
When I run the app, I get the following which is what I want
When I scroll the TableView and when rows disappear and reappear (or when I go to another viewController and come back) I get the following:
I don't understand why.
If I add this:
let width = UIScreen.mainScreen().applicationFrame.size.width
rowView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[rowView(==\(width))]", options: nil, metrics: nil, views: viewsDictionary))
It seems to solve the problem but doesn't work anymore when I go landscape.
Did I do something wrong when I did the layout?
EDIT:
When I print the description of all the constraints of rowView after adding them here is what I get:
<NSLayoutConstraint:0x17009f3b0 UIImageView:0x1701f5a00.leading == UITableViewCellContentView:0x1701968c0.leadingMargin>
<NSLayoutConstraint:0x17009f6d0 H:[UIImageView:0x1701f5a00]-(NSSpace(8))-[UILabel:0x13760f7f0'12 106,98']>
<NSLayoutConstraint:0x17009f9a0 H:[UILabel:0x13760f7f0'12 106,98']-(NSSpace(8))-[UIButton:0x137634b60]>
<NSLayoutConstraint:0x17009dec0 UITableViewCellContentView:0x1701968c0.trailingMargin == UIButton:0x137634b60.trailing>
<NSLayoutConstraint:0x170098920 V:|-(1)-[UIImageView:0x1701f5a00] (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x17009a4a0 V:[UIImageView:0x1701f5a00]-(1)-| (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x174098dd0 V:|-(1)-[UILabel:0x13760f7f0'12 106,98'] (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x174099550 V:[UILabel:0x13760f7f0'12 106,98']-(1)-| (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x170283c00 V:|-(1)-[UIButton:0x137634b60] (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x1702837a0 V:[UIButton:0x137634b60]-(1)-| (Names: '|':UITableViewCellContentView:0x1701968c0 )>
<NSLayoutConstraint:0x170283d90 V:[UITableViewCellContentView:0x1701968c0(<=68)]>
<NSLayoutConstraint:0x170283de0 V:[UITableViewCellContentView:0x1701968c0(>=67)]>
I post the solution I found as it might help. Thanks Ram for your help.
Indeed, when you're using auto layout programmatically, don't forget to add setTranslatesAutoresizingMaskIntoConstraints(false) on every component you are adding a constraint to.
In my case I had created the constraints in the Interface Builder, and as I needed to remove a UILabel from my rows during run time, I had to recreate the constraints for my rows (such as aspect ratio, width or height). So as I wanted to change ONLY the placement of my components in my rows, I didn't need to recreate the constraints for flagImageView or historyButton as long as I don't remove them.
But I needed to remove the constraints from my rowView as I was adding new ones and removing some. This rowView is actually the contentView of my UITableViewCell. By removing the constraints, I was also removing the constraints linked to its superView which is actually the UITableViewCell itself. So my contentView was not the same size as its parent. So I just needed to add these two lines:
rowView.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[rowView]-0-|", options: nil, metrics: nil, views: viewsDictionary))
rowView.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[rowView]-0-|", options: nil, metrics: nil, views: viewsDictionary))

Resources