Error using activateConstraints on a collection of Container View constraints - ios

In my iOS 8 app using size classes, to get the iPad to have a different layout in portrait and landscape I have an IBOutletCollection for each orientation that I activate and deactivate. This has worked great up until the introduction of a Container View.
I'm adding a new VC that is a separate tab item in the iPhone version, but I embed it into the iPad layout using a Container View. I'll focus on just one size class, RegularRegular. The container view has 4 constraints: Trailing, Leading, Top, and Bottom. When I run the app, it looks fine (in that single orientation).
Now, when I add these 4 constraints to my IBOutletCollection, the activateConstraints message fails with the following error message:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with items ; layer = ; contentOffset: {0, 0}; contentSize: {460, 78}> and > because they have no common ancestor. Does the constraint reference items in different view hierarchies? That's illegal.'
The constraints don't reference items in different view hierarchies. Top, Bottom, and Leading are all in relation to another view that is also in the VC's view. The Trailing is in relation to a stepper that is also in the VC's view. So they're all at the same hierarchal level to my knowledge.
When in debug, I can see that the constraint starts off with its active property as nil. This appears to be normal (the non-container constraints started off the same prior to them being set to YES). I checked this with a for loop setting the active property.
Is there something I'm doing wrong with the constraints on the Container View? Are constraints on a Container View somehow special because they are on a Container View? I've tried finding the answer myself, but I can't seem to find anything in regard to this issue I'm having.
Any help would be greatly appreciated.
4/10 Edit to provide pictures & code. Note at this time I've broken out the container constraints into a separate array so I can deal with them on their own.
In the image below, the container's Leading, Top, and Bottom constraints are in relation to the view to the right of the container. The Trailing constraint is in relation to the stepper above it.
Related code:
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *regularAnyConstraints;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *regularAnyContainerConstraints;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *anyRegularConstraints;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *anyRegularContainerConstraints;
These are the constraints in the _anyRegularContainerConstraints array:
[NSLayoutConstraint deactivateConstraints:_regularAnyConstraints];
[NSLayoutConstraint activateConstraints:_anyRegularConstraints];
[NSLayoutConstraint deactivateConstraints:_anyRegularContainerConstraints];
[NSLayoutConstraint activateConstraints:_anyRegularContainerConstraints];

When in debug, I can see that the constraint starts off with its active property as nil
That remark makes me wonder whether you might be misinterpreting what activateConstraints and deactivateConstraints do. Despite their names, and despite the misleading entry in the documentation, they actually add and remove constraints. Thus, instead of confusing yourself with these commands, I would suggest you might do better to call addConstraints and removeConstraints directly. The advantage of doing this is:
These latter are UIView methods, so you will be in conscious control of what view the constraints are added to.
When removing, you will be clear on your responsibility to retain the removed constraints if you want to use them again later.
I realize that this doesn't answer your question directly, but you didn't provide enough info for that (you didn't show your code that calls activateConstraints and deactivateConstraints, you didn't show any diagram of the view controller and view hierarchies, etc.) so it is not entirely clear where you are going wrong. Instead, I'm trying to get you to reorient your thinking as you try to work out the details of the problem.

The issue was that the container view was not 'installed' at the time the constraints were being applied. I am de/activating out of viewWillLayoutSubviews so at that time the size class must not yet be determined.
To resolve this I now have the Container View installed for AnyAny. Then I deselected all of the size classes that I don't want it present for.
I eventually discovered this was the issue because my Container View's UIView was not in self.view.subviews.

Related

Working with constraints in storyboard and code

I have a button and a view, when the button is not appearing I want the view to be extended (using top constraint) to fill up the white space, the button appears I want the view to shrink to make room for the button. The problem is I am getting this error:
failed: caught "NSInvalidArgumentException", "NSLayoutConstraint for (null): Constraint must contain a first layout item"
I set constraints in storyboard, and declare the top constraint by this:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *containerTopConstraint;
This is how I change the value of the constraints:
[_containerTopConstraint setConstant:20];
How can I fix this error?
I'd try putting your views inside of a UIStackView. It automatically repositions views when the isHidden property changes on one of them to take up the empty space.

Autolayout issues when adding a new view into a hierarchy

I'm struggling big time with a couple of scenarios when i have a UIView with constraints applied via IB and adding a new UIView into it's view hierarchy. Here's how it goes:
I have a full screen UITableView with the following constraints applied so it scales nicely through all the resolutions of iOS devices:
Now i'd like to add a UIView above the UITableView acting as a sort-of toolbar.
Right now the only possible way i succeeded in adding this view is adding it on top of the TableView, so that it covers the top of tableview. What i'd like to achieve is the newly added UIView pushing table down so both are visible. I tried several things including adding a container view in IB just above the tableview however that didn't work at all.
So my question is: is there a way to dynamically edit, remove and add new constraints to the view hierarchy, ideally supporting animation?
You can create IBOutlets for a constraint, just like any view.
Each NSLayoutConstraint object has a constant property that you can set anytime in code (it's the value of the constraint).
So you would create both views on Interface Builder, and constraint the top of the table view to the bottom of the new view.
The new view will have a set height constraint, and you create an outlet to that height constraint to make that view appear or disappear in code.
There are other possible solutions but I think this one is the easiest.
The code would be something like this:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *topHeightConstraint;
To set the value:
self.topHeightConstraint.constant = 60;
and to animate it:
[UIView animateWithDuration:0.6 animations:^{
self.topHeightConstraint.constant = 60;
[self.view layoutIfNeeded];
}];

How can I rearrange views when autorotating with autolayout?

I frequently run into this problem where I want my views arranged in one manner for portrait and a somewhat drastically different manner for landscape.
For a simplified example, consider the following:
Portrait:
Landscape:
Notice how in portrait the images are stacked vertically, but in landscape, they are side-by-side?
Of course, I can manually calculate the positions and sizes, but for more complicated views, this quickly becomes complex. Plus, I'd prefer to use Autolayout so there's less device-specific code (if any at all). But that seems like a lot of Autolayout code to write. Is there a way to do set up the constraints for this sort of stuff through the storyboard?
There are two underused features of Xcode's interface builder we can make use of here to make exactly this sort of thing a synch.
You can create IBOutlet connections for NSLayoutConstraints.
You can hook up "outlet collections", which is an array of IBOutlet objects.
So with these two things in mind, the gist of what we want to do is create all of our autolayout constraints for one orientation, hook them all up in an outlet collection. Now, for each of these constraints, uncheck the "Installed" option on interface builder. Then make all of our outlet collections for another layout, and hook them up to another outlet collection. We can create as many of these layout groups as we want.
It's important to note that we will need a reference to any UI element which has constraints installed on it directly, and we will need a seperate outlet collection not just for each layout we want, but for each UI object which has constraints installed on it directly.
Let's take a look at the fairly simplified example from your question.
If you look in the constraints list on the left, you can see half of them are grayed-out. The grayed-out constraints are the landscape constraints. I created all of these, then unchecked "Installed" for each of them:
Meanwhile, the ungrayed, normal looking constraints are the portrait constraints. For these, I left them "Installed". It's completely unnecessary to leave any of them installed, but you will run into problems if you leave multiple sets installed (they most likely conflict).
Be sure not to check "Remove at build time" for any of these. We don't want the constraints "removed" at build time. This simply means the constraint isn't created at all (so we'll lose the reference to it). If we leave this check marked while we have an IBOutlet to the constraint, Xcode will generate a warning:
Unsupported ConfigurationConnection to placeholder constraint. Constraints marked as placeholders in IB should not have any connections since these constraints are not compiled into the document and will not exist at runtime.
Anyway, so now we need to hook up the constraints to an outlet so we can access them at run time.
Hold Ctrl and click and drag from one of the constraints to your source code file, just as you would connect any other UI element. On the dialog that pops up, choose Outlet Collection and a descriptive name:
Now hook up all of the other constraints that match that constraint group into the same outlet collection:
Once we've finished hooking up all of our constraints, it's just a matter of removing/adding them at the appropriate time.
For such a simple example as the scenario described in the question, we can override updateViewConstraints with some code like this:
Swift
class ViewController: UIViewController {
#IBOutlet var landscapeConstraints: [NSLayoutConstraint]!
#IBOutlet var portraitConstraints: [NSLayoutConstraint]!
override func updateViewConstraints() {
let isPortrait = self.view.bounds.width < self.view.bounds.height
self.view.removeConstraints(self.portraitConstraints)
self.view.removeConstraints(self.landscapeConstraints)
self.view.addConstraints(isPortrait ? self.portraitConstraints : self.landscapeConstraints)
super.updateViewConstraints()
}
}
Objective-C
#interface ViewController()
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *landscapeConstraints;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *portraitConstraints;
#end
#implementation ViewController
- (void)updateViewConstraints {
BOOL isPortrait = self.view.bounds.size.width < self.view.boudns.size.height;
[self.view removeConstraints:self.portraitConstraints];
[self.view removeConstraints:self.landscapeConstraints];
[self.view addConstraints:(isPortrait ? self.portraitConstraints : self.landscapeConstraints)];
[super updateViewConstraints];
}
#end
We're not checking which set of constraints the view previously had, so just remove both of our constraint sets, and then add the appropriate set we want to use.
This is all the code we need to manage completely changing out an entire set of constraints on an object at run time. This allows to work in the interface builder to set all of our constraints up instead of having to do it programmatically (which I find a little more tedious and error-prone).
The end result? Very nice looking autorotation rearrangement without pulling your hair out getting the autolayout code done perfectly:
Now that UIStackView is available in iOS9, a simpler and guaranteed solution would be to add both views to a stackview, link the stackview to an IBOutlet and then add the following code to the viewcontroller:
- (void) adjustViewsForOrientation:(UIInterfaceOrientation) orientation {
if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown || orientation == UIInterfaceOrientationUnknown)
self.stackView.axis = UILayoutConstraintAxisVertical;
else
self.stackView.axis = UILayoutConstraintAxisHorizontal;
[self.stackView setNeedsLayout];
[self.stackView setNeedsDisplay];
}
and call this method from
-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration

Nested UIScrollViews and AutoLayout

I have seen several similar issues here in StackOverflow regarding UIScrollView and AutoLayout but now is my turn because I can't make it work properly.
My idea is to have an inner UIScrollView that scrolls horizontally (contains a set of images) and an outer UIScrollView that apart from the inner above, contains multiple UILabel, UITextView objects and scrolls vertically.
Without AutoLayout both are acting like expected but unfortunately size and origin are not right. With AutoLayout, outer UIScrollView, scrolls a bit but inner not moving at all. I can confirm that inner UIScrollView has the correct ContentSize during viewDidLoad but later in code has 0,0.
Below is a screenshot with views and associated Constrains.
Any idea?
UPDATE
For others who may face the same problem as mine.
In my case seems that the UIPageControl was messing up the whole thing. As you can see in the screenshot, I had the pager as a subview of the inner UISrollView WRONG!!!
I've simply move out the UIPageControl (became subview of the outer) and left Xcode to do the rest (Editor > Resolve Auto Layout Issues > Add Missing Constrains In View Controller). After that, UIScrollViews took tare of their selves (as Apple states in documentation). I've spent so many hours on this. I hope my solution saves others' time.
I am working on Xcode5
I've solved UIScrollView content issues with 2 steps:
Select UIScrollView, go to Editor>Resolve Auto Layout Issues>Add missing constraints
Add a IBOutlet NSLayoutConstraint for the height of whatever you are going to put inside the UIScrollView e.g. UIWebView. Hook up this outlet to the Height constraint you add via storyboard.
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *heightConstraint;
//set the constant to the element once it has been drawn
self.heightConstraint.constant = frame.size.height;
Hope this helps.

UILabel created using Interface Builder wrong origin for the frame

I am having a very hard time understanding that when I place a UILabel in interface builder and then get the coordinates in my code like this:
self.myLabel.frame.origin.x
I always get 0. Why is that even though I have placed the UILabel on the center of the screen? How can I get the correct coordinates?
The UILabel is being added to a UIScrollView. The UIScrollView covers the whole iPhone screen 320X480. UIScrollView is added through interface builder.
NSLog(#"%f",self.scrollView.bounds.size.height); returns 0
The origin is always relative to the parent view, in your case the UIScrollView. The way a UIScrollView works is that its contents can have a fixed origin but still appear in different places as the content size and offset (scroll position) are changed.
Furthermore, you did not say anything about the size of your UILabel, so it's also possible that the text within is centered but the view itself is much larger.
Also, you don't say where in your code you are checking the coordinates; if you check them too soon they may not yet be set.
Perhaps you are checking it before the label has been added to the view. Try putting a breakpoint somewhere in your code when you can actually see the label on screen and checking the values then.
Are you sure that self.myLabel is not nil, and that you've connected it correctly to your code? Also that you are loading the view from a xib file and not just using alloc/init on a view...
If non of the previous answers solved your problem,
Make sure you connect the label from the interface builder with its IBOutlet in the interface header file in your case the header should contain
#property (nonatomic,weak) IBOutlet UIlabel* myLabel;
and connected from the interface connector correctly with this property

Resources