'NSInvalidArgumentException', reason: 'Unable to parse constraint format' - ios

I have a subview that I want to keep stops during rotating screen, so I decided to put the NSLayoutConstraint type:
Trailing Space to Superview
Top Space to Superview
Button Space to Superview
I'm in a subclass of UITableViewCell. I wrote the code but I get the following error:
'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
self is not a key in the views dictionary.
H:[self.arrows]-5-|
My code in CustomCell.m is:
self.arrows = [[Arrows alloc]initWithFrame:CGRectMake(self.contentView.bounds.size.width-30, self.bounds.origin.y+4, 30, self.contentView.bounds.size.height-4)];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(self.arrows, self.contentView);
NSMutableArray * constraint=[[NSMutableArray alloc]init];
[constraint addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"H: [self.arrows]-5-|" options:0 metrics:nil views:viewsDictionary]];
[constraint addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-1-[self.arrows]" options:0 metrics:nil views:viewsDictionary]];
[constraint addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"[V: [self.arrows]-1-|" options:0 metrics:nil views:viewsDictionary]];
[self.arrows addConstraints:constraint];

It looks like that the autolayout visual format parsing engine is interpreting the "." in your VFL constraint to be a keyPath instead of a key like it's using valueForKeyPath:.
NSDictionaryOfVariableBindings(...) will take whatever your parameter is in the parenthesis and translate it into a literal key with the object as the value (in your case: #{"self.arrow" : self.arrow}). In the case of the VFL, autolayout is thinking that you have a key named self in your view dictionary with a subdictionary (or subobject) that has a key of arrow,
#{
#"self" : #{ #"arrow" : self.arrow }
}
when you literally wanted the system to interpret your key as "self.arrow".
Usually, when I'm using a instance variables getter like this, I typically end up creating my own dictionary instead of using NSDictionaryOfVariableBindings(...) like so:
NSDictionary *views = #{ #"arrowView" : self.arrow }
or
NSDictionary *views = NSDictionaryOfVariableBindings(_arrow);
Which would allow you to use the view in your VFL without the self and you still know what you're talking about:
NSArray *arrowHorizConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[arrowView]-5-|" options:0 metrics:nil views];
or
NSArray *arrowHorizConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[_arrow]-5-|" options:0 metrics:nil views];
As a general rule, I've learned not to have dictionary keys with a dot (.) in them to avoid any system confusion or debugging nightmares.

My trick is to simply declare a local variable that's just another pointer to the property, and put it in the NSDictionaryOfVariableBindings.
#interface ViewController ()
#property (strong) UIButton *myButton;
#property (strong) UILabel *myLabel;
#end
...
UIButton *myButtonP = self.myButton;
UILabel *theLabelP = self.myLabel;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(myButtonP, myLabelP);
The P suffix is for "pointer".

Easiest solution is to avoid the getters for variables from your own class and redefine variables from superclasses as local variables. A solution for your example is
UIView *contentView = self.contentView;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_arrows, contentView);

Make sure you add the constraints after adding the required subview to your main view.It took a while get knowledge relating to this issue .

Related

ObjC, revert to interface builder autolayout constraints, after adding / removing programmatically?

I have a free version of my app and I remove + add autolayout constraints (to hide a premium feature), however if the user purchases my app I'd like to revert back to to the constraints set by interface builder.
I'm hoping there's a method which will achieve this, but I haven't been able to find it so far?
Here's what I have
if (!purchased) {
[self.tblOtherAccounts addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[tblOtherAccounts(==0)]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(tblOtherAccounts)]];
NSDictionary *views = #{ #"tblOtherAccounts" : self.tblOtherAccounts,
#"butBackAllAc" : butBackAllAc };
[self.view removeConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[tblOtherAccounts]-(12)-[butBackAllAc]"
options:0
metrics:nil
views:views]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[tblOtherAccounts]-(0)-[butBackAllAc]"
options:0
metrics:nil
views:views]];
}
Add an IBOutlet to store your constraint in your ViewController :
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *myConstraint;
Then update its value :
_myConstraint.constant = 100.0f;
[_myView setNeedsUpdateConstraints];
[_myView layoutIfNeeded];
Regarding your comment on setting the relation to "equals", I don't think you'll be able to do that.
One thing you can do though is use your storyboard-generated constraint as a placeholder :
NSLayoutConstraint *newConstraint = [NSLayoutConstraint constraintWithItem:_myConstraint.firstItem
attribute:_myConstraint.firstAttribute
relatedBy:_myConstraint.relation
toItem:_myConstraint.secondItem
attribute:_myConstraint.secondAttribute
multiplier:_myConstraint.multiplier
constant:_myConstraint.constant];
[_myConstraint.secondItem removeConstraint:_myConstraint];
[constraint.secondItem addConstraint:newConstraint];
Of course the code above will create an exact duplicate of your constraint, but you can edit it to change the appropriate values to fit your needs.
I think, you cannot do exactly that.
You need to perform that logic in code. I.e. apply some constraints, then, on user action, apply another set of constraints.
Example:
You have 2 constraints:
#property ... topConstraint;
#property ... leadingConstraint;
At the start you assign them some values:
topConstraint.constant = 100;
leadingConstraint.constant = 100;
After some user action, modify them:
topConstraint.constant = 150;
leadingConstraint.constant = 150;

autolayout's visual format does not work with iOS8

I have a simple ViewController that contains only its default view with a background image placed on it, I have the following updateViewConstraints method which works perfectly on iOS 7 devices (without any call to setNeedsXXX methods):
-(void) updateViewConstraints {
[super updateViewConstraints];
[self.view removeConstraints:self.view.constraints];
NSDictionary *viewsDict = #{#"bgImage":self.bgImageView};
NSArray * constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[bgImage]|" options:0 metrics:nil views:viewsDict];
[self.view addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[bgImage]|" options:0 metrics:nil views:viewsDict];
[[self.view addConstraints:constraints];
}
however, this method does not work as expected on iOS 8 devices, the background image is shifted up and didn't fit in place.
any ideas would be highly appreciated.
This line: [self.view removeConstraints:self.view.constraints]; removes more on iOS 8 than on iOS 7. The easies way to fix this it to add an array holding the constraints you add to the view and remove only those constraints.
Maybe you even can remove this line completely but this depends on your application.

Auto layout - complex constraints

I'm trying to display content in a table cell using auto layout, programmatically. I'd like for the content to display as follows:
[title]
[image] [date]
[long string of text, spanning the width of the table, maximum of two lines]
My code looks like this:
-(NSArray *)constraints {
NSMutableArray * constraints = [[NSMutableArray alloc]init];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_titleLabel, _descriptionLabel, _dateLabel, _ratingBubbleView);
NSDictionary *metrics = #{#"padding":#(kPadding)};
NSString *const kVertical = #"V:|-(>=0,<=padding)-[_titleLabel]-(<=padding)-[_ratingBubbleView]-(<=padding)-[_descriptionLabel]-(>=0,<=padding)-|";
NSString *const kVertical2 = #"V:|-(>=0,<=padding)-[_titleLabel]-(<=padding)-[_dateLabel]-(<=padding)-[_descriptionLabel]-(>=0,<=padding)-|";
NSString *const kHorizontalDescriptionLabel = #"H:|-padding-[_descriptionLabel]-padding-|";
NSString *const kHorizontalTitleLabel = #"H:|-padding-[_titleLabel]";
NSString *const kHorizontalDateLabel = #"H:|-padding-[_ratingBubbleView]-padding-[_dateLabel]";
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kVertical options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kVertical2 options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalDescriptionLabel options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalTitleLabel options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalDateLabel options:0 metrics:metrics views:viewsDictionary]];
return constraints;
}
This is the result:
OK, I'm not going to try a fix your code. I'm just going to create constraints that I would use to achieve your layout. I'll put the thought process in comments.
First get a nice vertical layout going...
// I'm just using standard padding to make it easier to read.
// Also, I'd avoid the variable padding stuff. Just set it to a fixed value.
// i.e. ==padding not (>=0, <=padding). That's confusing to read and ambiguous.
#"V:|-[titleLabel]-[ratingBubbleView]-[descriptionLabel]-|"
Then go through layer by layer adding horizontal constraints...
// constraint the trailing edge too. You never know if you'll get a stupidly
// long title. You want to stop it colliding with the end of the screen.
// use >= here. The label will try to take it's intrinsic content size
// i.e. the smallest size to fit the text. Until it can't and then it will
// break it's content size to keep your >= constraint.
#"|-[titleLabel]->=20-|"
// when adding this you need the option "NSLayoutFormatAlignAllBottom".
#"|-[ratingBubbleView]-[dateLabel]->=20-|"
#"|-[descriptionLabel]-|"
Try not to "over constrain" your view. In your code you are constraining the same views with multiple constraints (like descriptionLabel to the bottom of the superview).
Once they're defined they don't need to be defined again.
Again, with the padding. Just use padding rather than >=padding. Does >=20 mean 20, 21.5, or 320? The inequality is ambiguous when laying out.
Also, In my constraints I have used the layout option to constrain the vertical axis of the date label to the rating view. i.e. "Stay in line vertically with the rating view". Instead of constraining against the title label and stuff... This means I only need to define the position of that line of UI once.

Displaying views with programmatic auto layout

EDIT
I am using programmatic auto layout and this issue seems to be eluding me,
in this class
#interface FooterButtonView : UIView {
...
}
I am trying to line up two views side by side
- (void)setUpViewWithTwoElements:(UIView*)element1 :(UIView*)element2{
element1.translatesAutoresizingMaskIntoConstraints = NO;
element2.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary* views = #{#"element1":element1, #"element2":element2};
NSDictionary* metrics = #{#"buttonHeight":#30.0};
NSString* horizontalFormatString = #"H:|-[element1]-[element2]-|";
NSString* verticalFormatString = #"V:[element1(buttonHeight)]-|";
[self addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:horizontalFormatString
options: NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
metrics:nil
views:views]];
[self addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:verticalFormatString
options:nil
metrics:metrics
views:views]];
}
however neither elements is being displayed.
in init I am adding both subviews and then calling the above function. Both elements descend from UIButton.
Any suggestions are greatly appreciated. Thank you!
You should see something with the code you posted assuming that the init method that calls this code is itself being called (and FooterButtonView is being displayed with a non-zero size). One thing you're missing though is the relative horizontal sizes of the two views. With the code you have, there's no way for the system to know what size each of the elements should be, just that they should take up the whole width minus the standard spacings. If you want the two views to be the same size, then change to this,
NSString* horizontalFormatString = #"H:|-[element1]-[element2(==element1)]-|";

adding NSLayoutConstraint between two objects

I am having a problem adding a constraint between a web view and a tool bar. I am using the following code but getting an error.
NSDictionary *viewsDictionary6 = NSDictionaryOfVariableBindings(newWebView, self.bottomToolBar);
NSArray *constraint6 =[NSLayoutConstraint constraintsWithVisualFormat:#"V:[newWebView]-(0)-[bottomToolBar]" options:0 metrics:nil views:viewsDictionary6];
for (int i = 0; i<constraint6.count; i++)
{
[self.view addConstraint:constraint6[i]];
}
I am trying to copy a similar constraint that is automatically generated from IB.
<NSLayoutConstraint:0x8b876b0 V:[UIWebView:0x8c53560]-(0)-[UIToolbar:0x9844400]>
Any idea what I am doing wrong?
Properties passed into NSDictionaryOfVariableBindings() have a different name (I can't remember what it is).
If you pass a property in rather than an iVar for example self.bottomToolBar the key will be self.bottomToolBar however using a "." in the visual format will cause syntax errors when it is parsed.
Try passing the synthesized iVar in instead:
NSDictionary *viewsDictionary6 = NSDictionaryOfVariableBindings(newWebView, _bottomToolBar);
NSArray *constraint6 =[NSLayoutConstraint constraintsWithVisualFormat:#"V:[newWebView]-(0)-[_bottomToolBar]" options:0 metrics:nil views:viewsDictionary6];
[self.view addConstraints:constraint6];

Resources