Find in code a layout constraint created in IB - ios

I would like to animate, in code, a constraint that is created in IB. I want to find the constraint between the bottom LayoutGuide and the bottom edge of my UIView.
The constraint instances are remarkably opaque: firstItem and secondItem are AnyObject so there is a lot of casting. And apart from doing a string compare on _stdlib_getTypeName(), it's hard to see how I will decide which constraints involve the LayoutGuides.
Or should I just delete all constraints and re-create them on the fly? (But then what's the point of IB? Since my storyboard uses auto layout, I am obliged to add constraints in IB anyway.)

Click on the constraint in Interface Builder and create an IBOutlet for it, just as you would for a button, text view, etc.
You can also find it at runtime using something like this:
NSLayoutConstraint *desiredConstraint;
for (NSLayoutConstraint *constraint in putYourViewHere.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) { // Or whatever attribute you're looking for - you can do more tests
desiredConstraint = constraint;
break;
}
}

This should be simple. As has already been stated, create an IBOutlet for the constraint you want to animate:
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *verticalSpaceLayoutConstraint;
Then when you want to animate it (Also see: How do I animate constraint changes?) force a layout pass if needed in your view that has the constraint, update your constraint to value you want, do your UIView animation and force layout passes as needed for the animation's duration:
- (void)moveViewSomewhere {
[self.view layoutIfNeeded];
_verticalSpaceLayoutConstraint.constant = 10; // The value you want your constraint to have when the animation completes.
[UIView animateWithDuration:1.0
animations:^{
[self.view layoutIfNeeded];
}];
}

Related

NSLayoutConstraint IBOutlet does not respond to setActive:

I have a UIViewController and I want to dynamically modify some of its layout according to, say, the click of a button.
As a simplified example, let's say I have created UIView in IB (self.box). In IB, set a constraint that makes the view's width equal to 0.9 of the VC's view's width.
Then I programmatically add another constraint and save it as a strong property. It sets the view's width to 0.7 of the VC's view's width, and I remove the previous constraint.
This leads to conflicting constraint errors and to a completely broken view. Whether I add or remove first or use the .active property instead of adding / removing, the same happens.
Any idea on how to make this work? My actual case is more complicated, but I have checked and only modifying a view's width constraint in my UIViewController leads to the error, even though setting it to different multipliers in IB does not lead to any conflicts.
- (void) viewDidLoad
{
NSLayoutConstraint* lowWidth = [NSLayoutConstraint constraintWithItem:self.box attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:0.7 constant:0.f];
self.lowWidth = lowWidth;
[self.view removeConstraint:self.highWidth];
[self.view addConstraint:self.lowWidth];
}
The constraint conflict includes the following:
Probably at least one of the constraints in the following list is one you don't want.
Try this:
<NSLayoutConstraint:0x145efb0f0 UIView:0x145d8d330.width == 0.7*UIView:0x14755e890.width>,
<NSLayoutConstraint:0x1474589b0 UIView:0x145d8d330.width == 0.9*UIView:0x14755e890.width>
EDIT: calling setActive: on my IBOutlet constraint did not seem to work, so I removed the outlet and programmatically created the constraint instead. This time it works as intended, but I have no explanation as to why the outlet did not work. Note that setting to weak / strong does not change anything.
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *highWidth;
I'd use only one NSLayoutConstraint and modifying that constraint size.
self.highWidth?.contant = size
[self.view layoutIfNeeded]
or activating/deactivting constraints... NSLayoutConstraint class

Is there a way to add an identifier to Auto Layout Constraints in Interface Builder?

After doing some visual layout in Interface Builder I've created some constraints that I want to access at runtime. Is there a way to label or identify constraints in Interface Builder so they can be looked-up later?
The reason I want to do this is I need to perform some calculation base upon the constraints that are visually specified. I am aware that Apple has provided the Visual Format Language and I could specify the constraints programmatically in a controller. I rather not use this approach so I don't lose the design time feedback of IB.
Edit
Making a referencing outlet did work, but the question still stands.
Update:
As explained by Bartłomiej Semańczyk in his answer, there is now an Identifier field visible in the Attributes Inspector for the NSLayoutConstraint making it unnecessary to expose this field yourself. Just select the constraint in the Document Outline view or in the Storyboard view and then add an identifier in the Attributes Inspector on the right.
Earlier Answer:
Yes, this can be done. NSLayoutConstraint has a property called identifier than can be exposed in Interface Builder and assigned. To demo this, I created a Single View Application that has a single subview that is a red box. This subview has 4 constraints: width, height, centered horizontally in container, centered vertically in container. I gave the width constraint the identifier redBoxWidth by doing the following:
Click on the width constraint in the Document Layout View. Then in the Identity Inspector under User Defined Runtime Attributes, click on the + under Key Path. Change keyPath to identifier, change the Type Boolean to String, and set the Value to redBoxWidth.
Then in ViewDidLoad it is possible to find the constraint by name and change its value:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for subview in view.subviews as [UIView] {
for constraint in subview.constraints() as [NSLayoutConstraint] {
if constraint.identifier == "redBoxWidth" {
constraint.constant = 300
}
}
}
}
}
Since Xcode 7 you can do it in storyboard:
However if you set up your constraint in code, do the following:
let constraint = NSLayoutConstraint()
constraint.identifier = "identifier"
For these constraints you have set up in storyboard, but you must set identifier in code:
for subview in view.subviews {
for constraint in subview.constraints() {
constraint.identifier = "identifier"
}
}
Also you could link constraint to properties of your controller as you do for any other components. Just ctrl drag it into your code:
And then it will be accessible in code of your controller as property:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *myConstraint;
And you could change it value for example:
self.myConstraint.constant=100.;
Just adding on to #vacawama's answer. You can write a UIView category and pull out constraints using a simple function, as opposed to copy/pasting loops everywhere you need to find a constraint:
.h file:
#import <UIKit/UIKit.h>
#interface UIView (EasyAutolayout)
-(NSLayoutConstraint *)constraintForIdentifier:(NSString *)identifier;
#end
.m file:
#import "UIView+EasyAutolayout.h"
#implementation UIView (EasyAutolayout)
-(NSLayoutConstraint *)constraintForIdentifier:(NSString *)identifier {
for (NSLayoutConstraint *constraint in self.constraints) {
if ([constraint.identifier isEqualToString:identifier]) {
return constraint;
}
}
return nil;
}
#end
Take the IBOutlet of your auto layout constraint.
There is one property called constant in the NSLayoutConstraint class.
For eg, you've taken the IBOutlet of height constraint of any of the views from your IB, and you want to change it's height programmatically, all you need to do is:
constraint.constant = isMoreHeight ? height1 : height2;
After doing this, you need to update all other views in the view hierarchy of the superview. To do this you'll need to write below line:
[self setLayoutIfNeeded];
For better user experience, you can put this line inside your animations block for smoother transition effects,
[UIView animateWithDuration:0.3f animations:^{
[self.view layoutIfNeeded];
}];
Hope this helps..
What about this:
if let myconstraint = self.view.constraints.filter( { $0.identifier == "myconstraintId" }).first {
// TODO: your code here...
}
my two cents for OSX (previous answers deal with UIKit..)
it work for OSX, too:
final private func getConstraintForButton(){
let constraints = self.myButton.constraints
for constraint in constraints {
if constraint.identifier == "redBoxWidth" {
constraint.constant = 300
break
}
}
}
Note: seems NOT working on custom NSView, instead...

Change frame programmatically with auto layout

I have a UITableView with Auto Layout and I need to reduce the height when the GADBannerView appears at the bottom of the screen.
Unfortunately with Auto Layout it is impossible to modify the frame. The solution is to remove Auto Layout and set the frame manually. This is very dangerous because all my apps works fine on 3.5' and 4.0' displays and removing Auto Layout adds a new testing phase and more effort.
Is there a way to change the frame even if Auto Layout is enabled?
Let your UITableView constraints to bottom layout is set to 0, make an IBOutlet. Now let your GADBannerView height is 40 so change your outlet.constant = 40; For more about how to make IBOutlet and change its value have a look into this or this hope this will help.
Edit: For those who seeking for example, follow these simple steps (Because this is accepted answer, I think it is worth to have an example. Credit to #manujmv for this example)
Create a height constraint for your view in your interface.
Then add an IBOutlet object in your class for this constraint.
For example:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint
*heightConstraint;
Connect this object in your connection panel.
Then change the value of this constraint whenever you needed
self.heightConstraint.constant = 40;
Rather than trying to change the frame of the view, add a height constraint using auto layout and reduce the value of this constraint. Do the below steps:
create a height constarint for your view in your interface.
Then add an IBOutlet object in your class for this constraint. for example,
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *heightConstraint;
Connect this object in your connection panel.
Then change the value of this constraint whenever you needed
self.heightConstraint.constant = 40;
One more thing, you have to call [self.view layoutIfNeeded]; method once you changne the constraints.
Enjoy :)
You can find out constraint like that
extension UIView {
var heightConstaint: NSLayoutConstraint? {
get {
for constraint: NSLayoutConstraint in constraints {
if constraint.firstAttribute == .height {
if constraint.relation == .equal {
return constraint
}
}
}
return nil
}
set{
setNeedsLayout()
}
}
}

iOS: CGAffineTransformScale moves my object

Building on a question I had earlier.
Simple button trying to transform a label. I want it to shrink by 0.5, which works but for some reason it also moves the object as it does it. The label jumps up and to the left, then transforms.
- (IBAction)btnTest:(id)sender
{
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
lblTest.transform = CGAffineTransformScale(lblTest.transform, 0.5f,0.5f);
}completion:^(BOOL finished) {
if(finished){
NSLog(#"DONE");
}
}];
}
I'm presuming from the question that you're using auto layout: In auto layout, if you have a leading and/or top constraint, after you scale with CGAffineTransformMakeScale, the leading/top constraint will be reapplied and your control will move on you in order to ensure that the constraint is still satisfied.
You can either turn off auto layout (which is the easy answer) or you can:
wait until viewDidAppear (because constraints defined in IB be applied, and the control will be placed where we want it and its center property will be reliable);
now that we have the center of the control in question, replace the leading and top constraints with NSLayoutAttributeCenterX and NSLayoutAttributeCenterY constraints, using the values for center property to set the constant for the NSLayoutConstraint as as follows.
Thus:
// don't try to do this in `viewDidLoad`; do it in `viewDidAppear`, where the constraints
// have already been set
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self replaceLeadingAndTopWithCenterConstraints:self.imageView];
}
// Because our gesture recognizer scales the UIView, it's quite important to make
// sure that we don't have the customary top and leading constraints, but rather
// have constraints to the center of the view. Thus, this looks for leading constraint
// and if found, removes it, replacing it with a centerX constraint. Likewise if it
// finds a top constraint, it replaces it with a centerY constraint.
//
// Having done that, we can now do `CGAffineTransformMakeScale`, and it will keep the
// view centered when that happens, avoiding weird UX if we don't go through this
// process.
- (void)replaceLeadingAndTopWithCenterConstraints:(UIView *)subview
{
CGPoint center = subview.center;
NSLayoutConstraint *leadingConstraint = [self findConstraintOnItem:subview
attribute:NSLayoutAttributeLeading];
if (leadingConstraint)
{
NSLog(#"Found leading constraint");
[subview.superview removeConstraint:leadingConstraint];
[subview.superview addConstraint:[NSLayoutConstraint constraintWithItem:subview
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:subview.superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:center.x]];
}
NSLayoutConstraint *topConstraint = [self findConstraintOnItem:subview
attribute:NSLayoutAttributeTop];
if (topConstraint)
{
NSLog(#"Found top constraint");
[subview.superview removeConstraint:topConstraint];
[subview.superview addConstraint:[NSLayoutConstraint constraintWithItem:subview
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:subview.superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:center.y]];
}
}
- (NSLayoutConstraint *)findConstraintOnItem:(UIView *)item attribute:(NSLayoutAttribute)attribute
{
// since we're looking for the item's constraints to the superview, let's
// iterate through the superview's constraints
for (NSLayoutConstraint *constraint in item.superview.constraints)
{
// I believe that the constraints to a superview generally have the
// `firstItem` equal to the subview, so we'll try that first.
if (constraint.firstItem == item && constraint.firstAttribute == attribute)
return constraint;
// While it always appears that the constraint to a superview uses the
// subview as the `firstItem`, theoretically it's possible that the two
// could be flipped around, so I'll check for that, too:
if (constraint.secondItem == item && constraint.secondAttribute == attribute)
return constraint;
}
return nil;
}
The particulars of your implementation may vary depending upon how you've defined the constraints of the control you want to scale (in my case, leading and top were based upon the superview, which made it easier), but hopefully it illustrates the solution, to remove those constraints and add new ones based upon the center.
You could, if you didn't want to iterate through looking for the constraint in question, like I do above, define an IBOutlet for the top and leading constraints instead, which greatly simplifies the process. This sample code was taken from a project where, for a variety of reasons, I couldn't use the IBOutlet for the NSLayoutConstraint references. But using the IBOutlet references for the constraints is definitely an easier way to go (if you stick with auto layout).
For example, if you go to Interface Builder, you can highlight the constraint in question and control-drag to the assistant editor to make your IBOutlet:
If you do that, rather than iterating through all of the constraints, you now can just say, for example:
if (self.imageViewVerticalConstraint)
{
[self.view removeConstraint:self.imageViewVerticalConstraint];
// create the new constraint here, like shown above
}
Frankly, I wish Interface Builder had the ability to define constraints like these right out of the box (i.e. rather than a "leading of control to left of superview" constraint, a "center of control to left of superview" constraint), but I don't think it can be done in IB, so I'm altering my constraints programmatically. But by going through this process, I can now scale the control and not have it move around on me because of constraints.
As 0x7fffffff noted, if you apply a CATransform3DMakeScale to the layer, it will not automatically apply the constraints, so you won't see it move like if you apply CGAffineTransformMakeScale to the view. But if you do anything to reapply constraints (setNeedsLayout or do any changes to any UIView objects can cause the constraints to be reapplied), the view will move on you. So you might be able to "sneak it in" if you restore the layer's transform back to identity before constraints are reapplied, but it's probably safest to turn off autolayout or just fix the constraints.

iOS: How does one animate to new autolayout constraint (height)

I've never worked with autolayout constraints before. I have a small new app I'm working on and noticed that the NIB's views are defaulting to autolayout. So, I figured I'd take the opportunity to work with it and try to figure out where Apple is going with this.
First challenge:
I need to resize an MKMapView and I'd like to animate it to the new position. If I do this the way I'm used to:
[UIView animateWithDuration:1.2f
animations:^{
CGRect theFrame = worldView.frame;
CGRect newFrame = CGRectMake(theFrame.origin.x, theFrame.origin.y, theFrame.size.width, theFrame.size.height - 170);
worldView.frame = newFrame;
}];
...then the MKMapView will 'snap' back to its original height whenever a sibling view gets updated (in my case a UISegmentedControl's title is being updated [myUISegmentedControl setTitle:newTitle forSegmentAtIndex:0]).
So, what I think I want to do is change the constraints of the MKMapView from being equal to the parent view's hight to being relative to the top of the UISegmentedControl that it was covering: V:[MKMapView]-(16)-[UISegmentedControl]
What I want is for the MKMapView height to shorten so that some controls beneath the map view are revealed. To do so I think I need to change the constraint from a fixed full size view to one where the bottom is constrained to the top of a UISegmentedControl...and I'd like it to animate as view shrinks to new size.
How does one go about this?
Edit - this animation is not animating though the bottom of the view does move up 170 instantly:
[UIView animateWithDuration:1.2f
animations:^{
self.nibMapViewConstraint.constant = -170;
}];
and the nibMapViewConstraint is wired up in IB to the bottom Vertical Space constraint.
After updating your constraint:
[UIView animateWithDuration:0.5 animations:^{[self.view layoutIfNeeded];}];
Replace self.view with a reference to the containing view.
This works for me (Both iOS7 and iOS8+). Click on the auto layout constraint you would like to adjust (in interface builder e.g top constraint). Next make this an IBOutlet;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *topConstraint;
Animate upwards;
self.topConstraint.constant = -100;
[self.viewToAnimate setNeedsUpdateConstraints];
[UIView animateWithDuration:1.5 animations:^{
[self.viewToAnimate layoutIfNeeded];
}];
Animate back to original place
self.topConstraint.constant = 0;
[self.viewToAnimate setNeedsUpdateConstraints];
[UIView animateWithDuration:1.5 animations:^{
[self.viewToAnimate layoutIfNeeded];
}];
There is a very good tutorial from apple itself that explain how to use animation with autolayout.
Follow this link and then find the video named "Auto layout by example"
It gives some interesting stuff about autolayout and the last part is about how to use animation.
I have made this small demo available. It shows how auto-layout constraints can be changed and animated in a very simple example. Simply take a look at the DemoViewController.m.
Most people use autolayout to layout items on their views and modify the layout constrains to create animations.
An easy way to do this without a lot of code is creating the UIView you want to animate in Storyboard and then creating a hidden UIView where you want the UIView to end. You can use the preview in xcode to make sure both UIViews are where you want them to be. After that, hide the ending UIView and swap the layout constraints.
There is a podfile for swapping layout constrains called SBP if you don't want to write it yourself.
Here's a tutorial.
No need to use more IBOutlet reference of the constraint instead of this you can directly access or update already applied constraint either applied by Programmatically or from Interface Builder on any view using the KVConstraintExtensionsMaster library. This library is also managing the Cumulative behavior of NSLayoutConstraint.
To add Height Constraint on containerView
CGFloat height = 200;
[self.containerView applyHeightConstrain:height];
To update Height Constraint of containerView with animation
[self.containerView accessAppliedConstraintByAttribute:NSLayoutAttributeHeight completion:^(NSLayoutConstraint *expectedConstraint){
if (expectedConstraint) {
expectedConstraint.constant = 100;
/* for the animation */
[self.containerView updateModifyConstraintsWithAnimation:NULL];
}
}];

Resources