In my apps I'm working a lot with constraints, in most cases with animations as well. In certain circumstances I need to remove constraints and add new ones.
As I need to support iOS 7 as well, I'm not able to use the active property, which would else be the solution for me.
The way to remove constraints is to use the removeConstraint method on a UIView.
Is it possible to create a method like
constraint.remove()
so you don't have to know which view is taking care over the constraint?
What I do is create arrays of the constraints that I wish to be able to add/remove and simply use the following:
#property NSMutableArray *newConstraints;
Fill up the newConstraints
iOS7 and iOS8:
[self.viewToChange addConstraints:self.newConstraints];
[self.viewToChange removeConstraints:self.newConstraints];
or iOS8 only, use the new mechanism
[NSLayoutConstraint activateConstraints:self.newConstraints];
[NSLayoutConstraint deactivateConstraints:self.newConstraints];
With this you can apply a set, remove the set and apply a new set.
You can also create the initial list from the storyboard set if you can identify which constraints are which.
I've ended up using the method autoRemove provided by the PureLayout library: https://github.com/smileyborg/PureLayout
This method finds the commonSuperview using the firstItem or secondItem and removes the constraint from the correct view.
It resulted in this one liner:
containerTopConstraint.autoRemove()
No, not that I'm aware of. The automatic managing of the host view only came in iOS8.
An ugly implementation could loop over all constraints of all views to finde the view where it is on.
But normally it shouldn't be to hard to manage the constrains in a way that you always know on which view they are defined.
Related
My question is about the creation of constraints: why creating an array of constraints to be activated later with the activate method is more efficient than activating each one by one with .isActive = true?
Activating a direct constraint, we're basically changing an attribute, a variable that's already instantiated in the UIView we're tinkering with:
#available(iOS 9.0, *)
open var topAnchor: NSLayoutYAxisAnchor { get }
But the definition shows that using the Array is more efficient:
/* Convenience method that activates each constraint in the contained array, in the same manner as setting active=YES.
This is often more efficient than activating each constraint individually. */
#available(iOS 8.0, *)
open class func activate(_ constraints: [NSLayoutConstraint])
I don't see how to create an array of a variable that is already in scope and then pass it in a loop to activate it. How is this more efficient than enabling direct constraint?
When you have multiple constraints you need to activate, AutoLayout has no idea when you're going to be "done" activating them if you go one by one. This might lead it to perform some intermediate calculations between constraints, whose results are wasted by adding another constraint immediately afterwards. By providing AutoLayout a list of constraints at once, it can both make sure it doesn't perform any intermediate calculations in between activations, and can get a better idea of the changes being made, leading to potentially more accurate and performant calculations.
If you poke around in /System/Library/PrivateFrameworks/CoreAutoLayout.framework with a disassembler like Hopper, you might (currently) see:
The implementation of +[NSAutolayoutConstraint activateConstraints:] is a passthrough to the private +[NSAutolayoutConstraint _addOrRemoveConstraints:activate:]
+[NSAutolayoutConstraint _addOrRemoveConstraints:activate:], for each constraint, tries to get a reference to the layout engine that the constraint belongs to (-[NSLayoutConstraint _layoutEngine]), and
If it belongs to an engine, activates or deactivates the constraint by calling through to the engine's -[NSISEngine withAutomaticOptimizationsDisabled:] method
If it doesn't yet belong to an engine, it will activate the constraint directly
-[NSLayoutConstraint setActive:] itself also does a non-trivial amount of work, potentially searching for its _nearestAncestorLayoutItem to _findCommonAncestorOfItem:andItem: in order to insert into the right place. By doing this work inside of a context that has optimizations disabled, autolayout can avoid doing that unnecessary work for each and every one of the constraints in the list.
The amount of work that AutoLayout has to do for each of your constraints likely dwarfs the cost of creating an array to store them temporarily, though you can likely profile this in a live app to see what it might be costing you.
I want to change the relation of a constraint programmatically that is why I create a new one and replace the old one with it. I have an IBOutlet of my constraint which I want to change. As because I am using multi-os-engine my code for updating the constraint looks a little bit different to normal objectiv-c or swift.
setBasketTopConstraint(
NSLayoutConstraint.constraintWithItemAttributeRelatedByToItemAttributeMultiplierConstant(
basketTopConstraint().firstItem(),
basketTopConstraint().firstAttribute(),
NSLayoutRelation.LessThanOrEqual,
basketTopConstraint().secondItem(),
basketTopConstraint().secondAttribute(),
basketTopConstraint().multiplier(),
basketTopConstraint().constant()
)
);
My problem is, that I can't see any effect from changing the constraint. It seems like I need to refresh my view somehow. I called view().setNeedsLayout() and view().layoutIfNeeded() but with no success.
Any advice?
You need to ensure that you:
Install any new constraints and activate them
Deactivate and/or remove any old constraints
Call layoutIfNeeded()
Did you try adding the code to change constraints in view will layout subviews??
I usually set all my auto layout code in the updateCOnstratins method of my view controller for the constraints of all the subclasses defining the view. Then in the subviews I place my constraints in the updateConstraints methods there. This makes me have a property of every single view in my class so I can reference it later on after I set translates.... to false. But Im reading that you don't have to set it in updateConstraints. Just not I read an article where the person says an apple engineer said that if the constraints are only made once then you can put them pretty much where ever. Yet, if you have constrains that change during the views lifecycle you place them in updateConstraints? Here are the links http://swiftandpainless.com/where-to-put-the-auto-layout-code/ http://swiftandpainless.com/dont-put-view-code-into-your-view-controller/.
So where should It go? Was this just an old way of doing this and now it has changed?
What you said in your post is what you would generally want to do. Put any constraints that might change in updateConstraints. This also means you should keep a reference to them to be able to update them or remove/replace them. Any static ones can be put after your initialization code (the init method of a UIView or the viewDidLoad method of a UIViewController, for instance). The only real requirement there is you can only add constraints to views that are actually in a view hierarchy together, so anytime after you've added the appropriate views would be fine.
There is usually no reason not to put your constraint creation code in viewDidLoad, which has the advantage of being called only once. For constraints that change, I like to associate that code with whatever directly precipitates the change, such as a change in size class or the removal or insertion of a view.
I want to practice creating simple apps using no storyboard. I am able to do the constraints programmatically (slowly) but I want to also practice separating my code into MVC. Is there a particular place/method that I am supposed to write the programatic constraints? Or does it not matter?
Good discussion in the comments. My thoughts, based on that discussion?
With an understanding that the question is subjective, you place your constraints:
The earliest in a view controller's life cycle where they work.
As "close" to the view as possible.
If it's something common, make it as universal as possible.
Understand how your specifics fit into everything.
(Understand, the question isn't limited to constraints. It could apply to hierarchies, UI, even database tables when you get down to it!)
Sticking to constraints, and my answer....
(1) Use the UIViewController and UIView lifecycles.
Generally the view life cycle is loadView, viewDidLoad, viewWillAppear, viewWillLayoutSubviews, viewDidLayoutSubviews, and viewDidAppear. great SO answer detailing this.
I believe that loadView is too early for constraints, but not viewDidLoad - **provided you aren't expecting to know the frame size. While many say viewDidLayoutSubviews is the right place for that, I've found that viewWillLayoutSubviews most times works just as well. Either way, get your constraints set as soon as possible!
(2) Do it as close to the view as possible.
If you have subviews - I have a "ToolBar" class of objects - you want the constraints, at least as much as possible, to be coded inside the class. For instance, in my tool bar, it slides out, has buttons, and even rotates upon orientation. The only constraints not inside these classes is for orientation - that owner is (and I believe should be) the view controller instantiating it.
(3) Make it universal.
I plan to use this tool bar across a few apps. So the first thing I did was add it to a framework. This framework was needed because I had an app that I delivered a photo editing exension - and the "edit" screen is as much the same as possible. In the end I move all my constraints there. (At least as much as possible.) Anything that I believe is reusable.
(4) Understand the specific requirements of your app.
This should be obvious. If you need to code for various orientations, use arrays and activate/deactivate them. (YES, a common mistake is replacing them! That's setting yourself up for some major headaches.)
If you can keep things active, declare the constraint, set `isActive = true1, and forget about it. If you need to adjust that constraint's constant or multiplier, in the declaration name it and then where you need to alter it, do it.
My conclusion? Auto layout is a very useful tool - more so in code. But the placement of code is like asking "how does one code an OOP app for auto rentals" or " how does one design a database for auto rentals". It not just an art, there are many answers. These are the "rules" I try to follow - YMMV.
To get started with this style of development I recommend checking out Let's Build That App as he goes through very in-depth examples of setting up complex apps entirely in code, without storyboards.
The way he structures the constraints is using a custom implementation of UIView, that way your view code is separated from the ViewController. Then, in the viewDidLoad method you can instantiate your implementation of UIView with something like self.view = MyView().
I wrote a few apps like this. The major drawbacks are that it can become very difficult to make quick adjustments, and you really need to learn about all the different types of constraints you can use.
Here's a pastebin of some extensions I used when doing this. I hope this helps.
I've set up multiple sets of constraints in IB, and I'd like to programmatically toggle between them depending on some state. There's a constraintsA outlet collection all of which are marked as installed from IB, and a constraintsB outlet collection all of which are uninstalled in IB.
I can programmatically toggle between the two sets like so:
NSLayoutConstraint.deactivateConstraints(constraintsA)
NSLayoutConstraint.activateConstraints(constraintsB)
But... I can't figure out when to do that. It seems like I should be able to do that once in viewDidLoad, but I can't get that to work. I've tried calling view.updateConstraints() and view.layoutSubviews() after setting the constraints, but to no avail.
I did find that if I set the constraints in viewDidLayoutSubviews everything works as expected. I guess I'd like to know two things...
Why am I getting this behavior?
Is it possible to activate/deactivate constraints from viewDidLoad?
I activate and deactivate NSLayoutConstraints in viewDidLoad, and I do not have any problems with it. So it does work. There must be a difference in setup between your app and mine :-)
I'll just describe my setup - maybe it can give you a lead:
I set up #IBOutlets for all the constraints that I need to activate/deactivate.
In the ViewController, I save the constraints into class properties that are not weak. The reason for this is that I found that after deactivating a constraint, I could not reactivate it - it was nil. So, it seems to be deleted when deactivated.
I do not use NSLayoutConstraint.deactivate/activate like you do, I use constraint.active = YES/NO instead.
After setting the constraints, I call view.layoutIfNeeded().
Maybe you could check your #properties, replace weak with strong.
Sometimes it because active = NO set self.yourConstraint = nil, so that you couldn't use self.yourConstraint again.
override func viewDidLayoutSubviews() {
// do it here, after constraints have been materialized
}
I believe the problem you are experiencing is due to constraints not being added to their views until AFTER viewDidLoad() is called. You have a number of options:
A) You can connect your layout constraints to an IBOutlet and access them in your code by these references. Since the outlets are connected before viewDidLoad() kicks off, the constraints should be accessible and you can continue to activate and deactivate them there.
B) If you wish to use UIView's constraints() function to access the various constraints you must wait for viewDidLayoutSubviews() to kick off and do it there, since that is the first point after creating a view controller from a nib that it will have any installed constraints. Don't forget to call layoutIfNeeded() when you're done. This does have the disadvantage that the layout pass will be performed twice if there are any changes to apply and you must ensure that there is no possibility that an infinite loop will be triggered.
A quick word of warning: disabled constraints are NOT returned by the constraints() method! This means if you DO disable a constraint with the intention of turning it back on again later you will need to keep a reference to it.
C) You can forget about the storyboard approach and add your constraints manually instead. Since you're doing this in viewDidLoad() I assume that the intention is to only do it once for the full lifetime of the object rather than changing the layout on the fly, so this ought to be an acceptable method.
You can also adjust the priority property to "enable" and "disable" them (750 value to enable and 250 to disable for example). For some reason changing the active BOOL didn't had any effect on my UI. No need for layoutIfNeeded and can be set and changed at viewDidLoad or any time after that.
The proper time to deactivate unused constraints:
-(void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.myLittleConstraint.active = NO;
}
Keep in mind that viewWillLayoutSubviews could be called multiple times, so no heavy calculations here, okay?
Note: if you want to reactive some of the constraints later, then always store strong reference to them.
When a view is being created the following life cycle methods are called in order:
loadView
viewDidLoad
viewWillAppear
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
Now to your questions.
Why am I getting this behavior?
Answer: Because when you try to set the constraints on the views in viewDidLoad the view does not have its bounds, hence constraints cannot be set. It's only after viewDidLayoutSubviews that the view's bounds are finalized.
Is it possible to activate/deactivate constraints from viewDidLoad?
Answer: No. Reason explained above.
I have found as long as you set up the constraints per normal in the override of - (void)updateConstraints (objective c), with a strong reference for the initiality used active and un-active constraints. And elsewhere in the view cycle deactivate and/or activate what you need, then calling layoutIfNeeded, you should have no issues.
The main thing is not to constantly reuse the override of updateConstraints and to separate the activations of the constraints, as long as you call updateConstraints after your first initialization and layout. It does seem to matter after that where in the view cycle.