How to swap two buttons' constraints programmatically? - ios

There are two buttons named buttonA and buttonB in a same view, I have set their constraints in storyboard. How can I exchange their constraints programmatically?

I was looking for an answer of this and I ended up doing it in the following way:
(Code examples are in Swift 3)
Let's say you have a UIView called containerView which contains two UIViews: subviewA and subviewB.
The constraints for the subviews are actually defined within containerView (as per the rule of the "closest common ancestor").
This solution also assumes that the constraints have as a 'First Item' subViewA or subViewB and as a 'Second Item' containerView. If your setup is different you might need to modify the code accordingly.
So we have a function that swaps constraints:
private func swapFirstItemsOfConstraintsDefinedInThisView(_ superview: UIView, betweenItem item1: AnyObject, andItem item2: AnyObject)
{
var constraintsToRemove: [NSLayoutConstraint] = []
var constraintsNew_item1: [NSLayoutConstraint] = []
var constraintsNew_item2: [NSLayoutConstraint] = []
for constraint in superview.constraints
{
if (constraint.firstItem === item1)
{
constraintsToRemove.append(constraint)
constraintsNew_item1.append(NSLayoutConstraint(item: item2, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: constraint.secondItem, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
}
else if (constraint.firstItem === item2)
{
constraintsToRemove.append(constraint)
constraintsNew_item2.append(NSLayoutConstraint(item: item1, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: constraint.secondItem, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
}
}
superview.removeConstraints(constraintsToRemove);
superview.addConstraints(constraintsNew_item1);
superview.addConstraints(constraintsNew_item2);
}
So then you can call the following (in your ViewController somewhere):
self.swapFirstItemsOfConstraintsDefinedInThisView(self.containerView, betweenItem: subviewA, andItem: subviewB)
self.containerView.layoutIfNeeded()
Note that the effects of the swap won't be shown unless you call the layoutIfNeeded(). Depending on your needs, you might want to add this call as the last line of the body of the swap function.
By the way if you call layoutIfNeeded() in an UIAnimation context (in the animations closure/block) it would animate the transition.
Note that there a lot of potential improvements of this code, it's just an example of the basic approach. For instance we could have an extension on NSLayoutConstraint to allow initting it by copying another NSLayoutConstraint and changing only the 'First item' property (so that we avoid the long repeated lines of code above). Also the swap function itself could be defined in an extension of UIView. And performance-wise there are potential issues with the above implementation.

I will assume that by Xcode you meant at runtime with code.
Constraints items (firstItem and secondItem) are readonly properties (i.e.: https://developer.apple.com/library/ios/documentation/AppKit/Reference/NSLayoutConstraint_Class/#//apple_ref/occ/instp/NSLayoutConstraint/firstItem).
So you can edit them after creating them.
You could create two set of constraints for each button and activate/desactivate via their active property (https://developer.apple.com/library/ios/documentation/AppKit/Reference/NSLayoutConstraint_Class/#//apple_ref/occ/instp/NSLayoutConstraint/active) the different set (which you can store in a IBOutletCollection) when desire.

Related

Swift4 issues : change Nslayoutconstraint programmatically

I would like to change position of UITextfield txtAmount NSLayoutConstraint programmatically from its top to bottom of collection view to bottom of image view. All views are embed in a ui view.
Old constraint is dragged and mapped from storyboard to view controller.
New constraint constr is to be created programmatically.
When it comes to implementation and execution, it says
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.
'Unable to install constraint on view. Does the constraint reference something from outside the subtree of the view? That's illegal.
Would you please tell me any guidelines for such modification ? I embed the UI elements in an embedded UIView because of scrollview I have used.
let constr = NSLayoutConstraint(item: txtAmount, attribute: .top, relatedBy: .equal, toItem: imageView, attribute: .bottom , multiplier: 1, constant: 0)
IETypeList.removeFromSuperview()
uiviewType.removeFromSuperview()
txtAmount.addConstraint(constr)
txtAmount.removeConstraint(constraintPo)
you are adding constraint to the txtAmount whereas you should've added it to the view which actually contains txtAmount and another view referenced in this constraint, i.e. imageView. Let's name this view which contains them superview.
let constr = NSLayoutConstraint(item: txtAmount, attribute: .top, relatedBy: .equal, toItem: imageView, attribute: .bottom , multiplier: 1, constant: 0)
IETypeList.removeFromSuperview()
uiviewType.removeFromSuperview()
txtAmount.removeConstraint(constraintPo)
superview.addConstraint(constr)
But this is not recommended way since iOS 8. As the docs say, you should set the constraint isActive (read this) property to true instead, and iOS will add them to relevant views:
IETypeList.removeFromSuperview()
uiviewType.removeFromSuperview()
constraintPo.isActive = false
constr.isActive = true

Dynamic number of elements in TableViewCell

I need to add labels one after another vertically and their number is dynamic. For now it works fine with this code:
let numUnitsLabel = UILabel(frame: CGRectZero)
numUnitsLabel.text = units
numUnitsLabel.font = UIFont.boldSystemFontOfSize(28)
numUnitsLabel.textColor = UIColor.redColor()
numUnitsLabel.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(numUnitsLabel)
let topConstraint = NSLayoutConstraint(item: numUnitsLabel, attribute: .Top, relatedBy: .Equal, toItem: cell.contentView, attribute: .Top, multiplier: 1, constant: 8 * CGFloat(i+1) + CGFloat(i*25) + CGFloat(i*20) - 5)
let rightConstraint = NSLayoutConstraint(item: numUnitsLabel, attribute: .Trailing, relatedBy: .Equal, toItem: cell.contentView, attribute: .Trailing, multiplier: 1, constant: -10)
cell.contentView.addConstraints([topConstraint, rightConstraint])
The cell is drawn fine. But when I add new item in the list and call tableView.reloadData the old labels are "remembered" and they are over new ones. The newly added object is put in the first place in the array and they are like one over another... Maybe my approach is bad, but if you have any suggestions please advice me.
Remember that cells are re-used, so however a cell was prepared previously, it still has all of that state. In particular, its view hierarchy (added subviews) remain. The reason for this is generally a layout of a Table Cell stays the same, and only its content is meant to be re-set in cellForRowAtIndexPath.
You're using an anti-pattern, first of all, which is why you're running into difficulty.
If you really want to clear out and re-layout the cell, remove all subviews explicitly, every time, e.g.:
https://techfuzionwithsam.wordpress.com/2014/12/23/what-is-the-best-way-to-remove-all-subviews-from-parent-viewsuper-view/
I suspect though, (admittedly this isn't CodeReview) that your design is flawed. Generally table cells don't need to grow in height with dynamic count of internal elements. You may need nested tables (!!), really just a single multi-line UITextView in each, or a table structured by sections with custom section header views.

iOS constraints doesn't allow to use multiplier

I am trying to layout some custom views and when I try to activate the constraints, Xcode says that I can't use multiplier. Here is an example of the code:
class MenuView: UIView {
var addButton: AddButton!
var settingsButton: SettingsButton!
// ........
func setConstraints (withBarReference reference: NSLayoutYAxisAnchor) {
NSLayoutConstraints.activateConstraints([
// ........
addButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 0.5),
// ........
settingsButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 1.5)
])
}
}
The thing here is that Xcode gives a syntax error on the contraintEqualToAnchor: functions and says that I should replace "multiplier" to "constant".
Why can't I use the multiplier option with the X center anchors?
You can't set multiplier using helper functions, but you can set multiplier using NSLayoutConstraint initializer. Just got stuck by this myself, but found the answer.
Your code: addButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 0.5)
Correct code: NSLayoutConstraint(item: addButton, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 0.5, constant: 0)
Also, don't forget to activate this constraint by typing isActive = true
Previous answers work very weird now.
You can simply create UILayoutGuide with multiplier width/height with view and set guide.trailing equal to the centerX of your subview.
For example, if you need to place the addButton in the first 1/3 of a view and settingsButton in 2/3 you can simply set two layout guides
let addButtonGuide = UILayoutGuide()
self.addLayoutGuide(addButtonGuide)
addButtonGuide.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1/3).isActive = true
addButtonGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
addButton.centerXAnchor.constraint(equalTo: addButtonGuide.trailingAnchor).isActive = true
// same for settingsButton but using 2/3 for the multiplier
But the really best way is to use UIStackView and set its distribution property to equalCentering.
Another option is to use uncommon Auto Layout API to create NSLayoutDimension between two centerXAnchors and make constraint to self.widthAnchor:
addButton.centerXAnchor.anchorWithOffset(to: self.centerXAnchor)
.constraint(equalTo: self.widthAnchor, multiplier: 0.25).isActive = true
self.centerXAnchor.anchorWithOffset(to: settingsButton.centerXAnchor)
.constraint(equalTo: self.widthAnchor, multiplier: 0.25).isActive = true
It seems that in IB you can use the multiplier option with Center X and obtain the effect you're looking for (set the center of button1 at 1/4 the width of the view it's in, and the center of button2 at 2/3 of the width of the view it's in):
.
I tried to use it both in code and in IB, and in code I got the same error as you.
Surprisingly, in IB it worked, no errors, no warnings. (I am using Xcode 7, will try it in Xcode 8 to see if it still works).
You can't use multipliers on NSLayoutXAxisAnchor anchors - multiplying by a position along a line doesn't make sense in a way that the constraints system can understand. You can only use multipliers with NSLayoutDimension anchors, which measure lengths, like the width.
The layout you are trying to make would be better achieved using a stack view.

Xcode swift view wrap content

I'm an Android developer trying my hand at Xcode and it's been unpleasant so far. What I'm trying to do is have a custom view that has three sub views:
UIImageView (for an icon)
UILabel (for the title)
UILabel (for the content)
I want it such that the content label's height grows and shrinks to wrap the text it contains (like Android's wrap_content). And then, I want the custom view to also grow and shrink to wrap all three sub views.
However, I cannot, for the life of me, figure out how these auto layouts/constraints work.
01) How would I make my UILabel's height grow/shrink to match its contained text?
02) How would I make my custom view's height grow/shrink to match its contained sub views?
override func layoutSubviews() {
super.layoutSubviews()
img_icon = UIImageView()
txt_title = UILabel()
txt_content = UILabel()
img_icon.backgroundColor = Palette.white
img_icon.image = icon
txt_title.text = title
txt_title.textAlignment = .Center
txt_title.font = UIFont(name: "Roboto-Bold", size:14)
txt_title.textColor = Palette.txt_heading1
txt_content.text = content
txt_content.textAlignment = .Center
txt_content.font = UIFont(name: "Roboto-Regular", size:12)
txt_content.textColor = Palette.txt_dark
txt_content.numberOfLines = 0
txt_content.preferredMaxLayoutWidth = self.frame.width
txt_content.lineBreakMode = NSLineBreakMode.ByWordWrapping
self.backgroundColor = Palette.white
addSubview(img_icon)
addSubview(txt_title)
addSubview(txt_content)
/*snip img_icon and txt_title constraints*/
let txt_content_x = NSLayoutConstraint(item: txt_content, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)
let txt_content_y = NSLayoutConstraint(item: txt_content, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 80)
let txt_content_w = NSLayoutConstraint(item: txt_content, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: 1, constant: 0)
let txt_content_h = NSLayoutConstraint(item: txt_content, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 40)
txt_content.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints([
txt_content_x,
txt_content_y,
txt_content_w,
txt_content_h
])
}
I understand that, in the above code I've tried, I have the height set to a constant 40. This is only because I don't know how to achieve what I want.
[EDIT]
I've tried setting the height constraint to greater than or equal to but it just crashes Xcode.
[EDIT]
It crashes Xcode if I try to view it but works perfectly fine in the simulator. Question now is, why?
My height constraint is now:
let txt_content_h = NSLayoutConstraint(item: txt_content, attribute: .Height, relatedBy: .GreaterThanOrEqual, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 40)
It works in the simulator and has the desired behaviour. However, if I open the storyboard that contains the view, it crashes. It's definitely that line of code because changing it back to .Equal resolves the crash.
[EDIT]
My temporary fix is:
#if TARGET_INTERFACE_BUILDER
//use .Equal for height constraint
#else
//use .GreaterThanOrEqual for height constraint
#endif
This way, it doesn't crash Xcode and still renders the way I want it on the simulator.
[EDIT]
I removed the pre-processor check because I realized there's no actual thing like that defined and it still works now. I swear I've changed nothing else.
I am this close to giving up on iOS development because the interface builder keeps crashing Xcode without a reason when everything works in the simulator. Then, I do some nonsense edits and it works fine again.
01) How would I make my UILabel's height grow/shrink to match its contained text?
Just set top, left and right-constraint to the labels superview. Set the property number of lines to 0. Then it will start wrapping text.
02) How would I make my custom view's height grow/shrink to match its contained sub views?
By using interface builder this is much easier to achieve.
My suggestion to you is to start with your constraints in storyboard. You will not need to compile your code to see what the constraints will result in. Also you will get warnings and errors directly in the interface builder.
If you WANT to use programmatic constraints, my suggestion is to start using a framework for it. For example: https://github.com/SnapKit/SnapKit
You can use a trick with constraints to achieve wrap-content. For example :
let maximumWidth = frame / 4 //For example
yourView.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
The "maximumWidth" depends on your UI and your design and you can change it.
Also, you should set "lineBreakMode" in StoryBoard or in code like :
yourBtn.titleLabel?.lineBreakMode = .byCharWrapping //For UIButton or
yourTxt.textContainer.lineBreakMode = .byCharWrapping //For UITextView
Often clean will do a lot of good when code jams for no reason ar all, cmd-shift-k if i remember correctly
I understand there is no direct application of wrap content in iOS just like we have in Android and thats a big problem, I resolved it through manual anchors like this.
create a function with where in you calculate the height of the view using
mainView.contentSize.height
and then set anchors based on the total height to the enclosing view, call this function inside
override func viewWillLayoutSubviews()
And this would work, the viewWillLayoutSubviews() is a lifecycle method and whenever you override you have to do
super.viewWillLayoutSubviews()
This worked in my case, might work with yours too, if there is a better approach please do comment.

Set constraint so that firstItem's top equals secondItem's height

I'd like to set a top constraint so that it has a relationship which is equal with another item's height.
I prepare it in interface builder so that item 1's top is equal to item 2's top.
Then, in code (because I don't think it can be done in Interface Builder), I try to set the constraint's secondAttribute property.
This seems logical to me based on a basic understanding of how constraints are composed (of two items, an attribute for each, a relationship type, and a constant), but it does not work:
#IBOutlet var fillTopToContainer: NSLayoutConstraint!
// ...
override func viewDidLoad() {
fillTopToContainer.secondAttribute = NSLayoutAttribute.Height
}
Swift compiler error:
Cannot assign to the result of this expression.
I have fiddled with the constant to make sure that topDistEqualsHeight contains the constraint I expect, and it does. The other values in the constraint are correct for my needs, I only want to change the attribute.
Is this a known limitation, a syntax issue, or a big piece of missing knowledge?
Update
I've also tried this, which throws a runtime error:
var pushTopDown = NSLayoutConstraint(item: self.view,
attribute: NSLayoutAttribute.Height,
relatedBy: NSLayoutRelation.Equal,
toItem: fillRect,
attribute: NSLayoutAttribute.Top,
multiplier: 1,
constant: -10)
self.view.addConstraint(pushTopDown)
This is the layout I'm trying to achieve. It's a scrollview which is exactly two screens tall, the bottom half has a fill color.
#P-double suggested that the fillRect match it's top to the bottom position of a full height object, which would work except you can't set the top of a grandchild relative to its grandparent in IB.
View Hierarchy:
frame (fills the screen, root view)
scrollView (fills the frame. Content size is determined by constraints of inside views)
fillRect (height==frame, bottom==scrollView.bottom, top==?)
Constraint's do not work in the way you are trying to use them, most notably constraints properties are all immutable apart from the constant property.
This pairing of constraints does not work, because one relates to an origin point (y-positon), and one relates to a size dimension. It's not clear what you are trying to achieve, but there will be other ways in which you can achieve your desired layout. If you want the second view to sit below the first (in the y-plane, it doesn't necessarily have to align centre-x positions), why not pin the bottom of the first to the top of the second? If you'd like to post some more details, I'll do my best to help.
EDIT
To achieve you desired layout, you should pin the top of the fillRect to the top of the scrollView, and give the constraints constant the value of the frame height. Such as this
var pushTopDown = NSLayoutConstraint(item: scrollView,
attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem: fillRect,
attribute: NSLayoutAttribute.Top,
multiplier: 1,
constant: self.view.height)
scrollView.addConstraint(pushTopDown)
Also notice that the constraint is added to the scroll view, not the view controllers view. You'll also want to the make the width of fillRect equal to the scrollViews frame width.
As #Rob points out, you'll need to make sure you haven't already added constraint for the top. Interface builder will complain though if the view is not fully constrained. The trick is to add a top constraint in interface builder, but to mark it as a design time constraint. To do this, select the constraint you want to replace in code, the open the attributes inspector on the right, and tick the 'Remove at build time' option. (See picture) This allows the xib/storyboard to compile without error, but doesn't actually add the constraint to the view.
Bottom line, if you try to define a constraint such that the "top" attribute of an item to be equal to the "height" attribute of another, you will receive an error that says:
Invalid pairing of layout attributes
Bottom line, you cannot define constraints between "top" and "height" attributes.
If you want to avoid using spacer views, the other technique to try when vertically spacing views is to set the .CenterYWithinMargins attribute with respect to the superview's .CenterYWithinMargins, applying the appropriate multiple. You can equally space views with judicious use of different multiple values for each item's respective .CenterY attribute.
A couple of observations regarding a few of your attempts: Notably, you cannot mutate secondAttribute of an existing constraint. Only the constant property may be modified after the constraint creation. Also, in your update to your question, you illustrate the attempt to create a new constraint, and you'll obviously want to make sure make sure you remove the old constraint (or define it with a lower priority) before you create a new one.
To illustrate the concept, here is a scroll view with a tinted view that is off screen all created programmatically (it's the most concise way to describe the constraints, methinks):
let scrollView = UIScrollView()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(scrollView)
let tintedView = UIView()
tintedView.setTranslatesAutoresizingMaskIntoConstraints(false)
tintedView.backgroundColor = UIColor.orangeColor()
scrollView.addSubview(tintedView)
let views = ["scrollView" : scrollView, "tintedView" : tintedView]
// vfl for `frame` of scrollView
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: nil, metrics: nil, views: views))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: nil, metrics: nil, views: views))
// vfl for `contentSize` of scrollView
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tintedView]|", options: nil, metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[tintedView]|", options: nil, metrics: nil, views: views))
// vfl for `frame` of tintedView
//
// (note, this can be integrated into the above constraints, reducing
// the extraneous VFL, but I implemented them as separate VFL to
// clearly differentiate between settings of the scrollView `contentSize`
// and the tintedView `frame`)
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[tintedView(==scrollView)]", options: nil, metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[tintedView(==scrollView)]", options: nil, metrics: nil, views: views))
// offset tinted view
scrollView.addConstraint(NSLayoutConstraint(item: tintedView, attribute: .CenterY, relatedBy: .Equal, toItem: scrollView, attribute: .CenterY, multiplier: 3.0, constant: 0.0))

Resources