How can I rearrange views when autorotating with autolayout? - ios

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

Related

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];
}];

Where do I update my constraint before the view is on screen?

I need to update some NSLayoutConstraints before the view comes on screen depending on the device's screen size.
What I want to do is that this update becomes invisible to the user's eyes.
And I do not know where I can do that because :
- if I do this in :
- (void)viewWillLayoutSubviews
of my UIViewController it is called twice so i make twice the calculation on my constraint and I do not want to set a bool or a counter...
- if I do this in :
- (void)viewDidLoad
it does not work because I calculate my constrain's value based on the view of the view controller which is 0 height at this moment.
I see that there are a lot of methods for auto layout (setNeedsUpdate ...)
and if someone could tell me the best way to achieve an update of the constraints only once in the viewController lifeCycle that would be great.
Thanks for any help! (N.B: the auto-layout concept is new to me.)
Try overriding updateViewConstraints on your view controller. (Don't forget to call super ;) )
See Apple docs here
You can create the outlet of your height constraint from storyboard.
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *heightForYourView;
& in viewDidLoad write the following code which will automatically change the size
//if you want to change the size as per device check device type in my case it iphone6
if (IS_IPHONE_6)
{
self.heightForYourView.constant=250;//height you want
}

Error using activateConstraints on a collection of Container View constraints

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.

how to change components in UIViewController on runtime while using auto layout in iOS

I have a case in which I need to manipulate UIViewController's component on runtime. The matter is I have adopted Auto Layout.
Now say I have a UIView called aView with Constraints plotted in a UIViewController.
Next, on the basis of my business logic, I need to remove aView and replace with other UIView called bView.
Here, when I remove aView, will all related constraints also removed OR it will remain as it is.
The question is if I simply remove aView and add bView will plot UIView same as before with old constraints or I need to re-implement all constraints?
Which is the best approach to change UIViews on runtime, considering using Auto Layout?
When you remove aView and replace with bView the constraints will be gone and you will have to add them again. You can't take the constraints that belonged to aView and apply them to bView, you'll have to recreate them for bView.
It might be possible for you to skip autolayout for bView completely if it is just going to take the place of aView. If, in this case, the layout system has run and found a frame for aView you can just reuse that frame for bView.
It might be simpler to just add both aView and bView when the viewController loads and then switch one or the other to be hidden.
Why don't you just create a public method for the constraint programmatically, and just need to call the method for each time you create a new viewcontroller rather than copy and paste the method. Other solution is, creating a superclass of all the uiview that you want to have the same constraint.

Stop autolayout from resizing my view

Using Storyboards I add a UIView with height of 5px.
At run time the height of that view is increased (could be up to 25px).
When the orientation changes, the height of the view is reverted back to the original 5px.
How can I prevent this? I want the height to remain at whatever it was prior to the orientation change.
Sure I could detect orientation change and then change it back but that looks sloppy because it shrinks then increases right away.
Edit: This may or may not be an autolayout issue. Or might just be the default behavior of storyboards.
Add a constraint fixing the height, you may also need to remove a constraint. If you show the code or explain how you're setting the constants in the first place can give a more detailed answer.
Other answer is to remove the constraint that stretching the size of your view, may be that you have conflicting constraints.
One more answer could be you need to adjust the priority of an other constraint perhaps one for spacing between views. Again more detail = better answer.
Overriding -layoutSubviews is the incorrect approach. If you're using autolayout you should adjust a view's height by modifying its constraints programmatically.
Make a property for your view's height constraint:
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *heightConstraint;
I put IBOutlet there since you said you're making the constraints in a storyboard. Just drag the height constraint to that property to connect it. (You could also create the constraint programmatically, in which case you wouldn't use IBOutlet.)
Then, whenever you want to change the height:
self.heightConstraint.constant = 25;
The solution to the problem is to sublcass UIView and override -(void)layoutSubviews
In -(void)layoutSubviews is where I set my desired height

Resources