What are your best practising when you subclass UIView?
I'm trying to do a view for a viewcontroller entirely in code, using the new ios 6 autolayout.
So I've subclassed UIView and setup the basic constraints in #initWithFrame (should I do this in #updateConstraints?). The problem is that I have an UILabel with dynamic text and variable width so I have to setup #preferredMaxLayoutWidth after the constraints have determined a frame, so I think #layoutSubviews is the best place to do that, but doing that results in a NSInternalInconsistencyException: Auto Layout still required after executing -layoutSubviews. MyView's implementation of -layoutSubviews needs to call super. (RuntimeError)
Before you asks I'm sure I have called super in every method I overridden.
Any idea?
Perhaps what you need here is to not have your view call [super layoutSubviews], but UIView, the ancestor, call its [super layoutSubviews].
This can be achieved not by subclassing, but implementing a category of UIView, which would have layoutSubViews function implemented, calling super. If I could have your code I could have tried. Someone has tried it successfully here.
Again, this does not guarantee that this will be fixed. There seems to be many bug reports concerning this in Apple forums. Unfortunately, we can't browse others' bugs in Apple portal, we can just file our own.
As a workaround, I would also suggest if any of your constraints conflict with your
preferredMaxLayoutWidth argument: try increasing or decreasing it. As per the overriding order of its own, it can change things, and who (other than Apple) knows - it maybe fixed. Fingers crossed!
This seemed to work for me, I can't explain why, but it did.
-(void)layoutSubviews
{
[super layoutSubviews];
layout code here, including updated constraints
[super layoutSubviews];
}
Related
I need to update a constraint programmatically in my project to adjust a view's height accordingly (based on different subviews). I've made outlet of the constraint in my controller but facing an issue.
when I try to update this for the first time (in viewDidLoad or viewWillAppear method), it's not updated. and if i update it afterwards (because of some rotation etc), then it is done correctly. Kindly tell me why this is happening and what is the right way/place to do this? (As i feel that the constraint is updated somewhere again after my updation in viewWillAppear/DidLoad).
I tried view.layoutIfNeeded as well but it didn't help. I guess it has something to do with viewDidLoad and other viewController delegate methods
P.S. I'm also using size classes in my project but I think it has nothing to do with that as it's working in some cases.
Updating constraints may not work in viewWillAppear.
It will, however, work in viewDidAppear.
There are other places you may overwrite, such as: (using static BOOL shouldRefreshView = NO; for the first time)
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(shouldRefreshView) {
shouldRefreshView = NO;
[self.view layoutIfNeeded];
}
}
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.
I see different examples where constraints are set. Some set them in viewDidLoad / loadView (after the subview was added). Others set them in the method updateViewConstraints, which gets called by viewDidAppear.
When I try setting constraints in updateViewContraints there can be a jumpiness to the layout, e.g. slight delay before the view appears. Also, if I use this method, should I clear out existing constraints first i.e. [self.view [removeConstraints:self.view.constraints]?
I set up my constraints in viewDidLoad/loadView (I'm targeting iOS >= 6). updateViewConstraints is useful for changing values of constraints, e.g. if some constraint is dependent on the orientation of the screen (I know, it's a bad practice) you can change its constant in this method.
Adding constraints in viewDidLoad is showed during the session "Introduction to Auto Layout for iOS and OS X" (WWDC 2012), starting from 39:22. I think it's one of those things that are said during lectures but don't land in the documentation.
UPDATE: I've noticed the mention of setting up constraints in Resource Management in View Controllers:
If you prefer to create views programmatically, instead of using a
storyboard, you do so by overriding your view controller’s loadView
method. Your implementation of this method should do the following:
(...)
3.If you are using auto layout, assign sufficient constraints to each of
the views you just created to control the position and size of your
views. Otherwise, implement the viewWillLayoutSubviews and
viewDidLayoutSubviews methods to adjust the frames of the subviews in
the view hierarchy. See “Resizing the View Controller’s Views.”
UPDATE 2: During WWDC 2015 Apple gave a new explanation of updateConstraints and updateViewConstraints recommended usage:
Really, all this is is a way for views to have a chance to make changes to constraints just in time for the next layout pass, but it's often not actually needed.
All of your initial constraint setup should ideally happen inside Interface Builder.
Or if you really find that you need to allocate your constraints programmatically, some place like viewDidLoad is much better.
Update constraints is really just for work that needs to be repeated periodically.
Also, it's pretty straightforward to just change constraints when you find the need to do that; whereas, if you take that logic apart from the other code that's related to it and you move it into a separate method that gets executed at a later time, your code becomes a lot harder to follow, so it will be harder for you to maintain, it will be a lot harder for other people to understand.
So when would you need to use update constraints?
Well, it boils down to performance.
If you find that just changing your constraints in place is too slow, then update constraints might be able to help you out.
It turns out that changing a constraint inside update constraints is actually faster than changing a constraint at other times.
The reason for that is because the engine is able to treat all the constraint changes that happen in this pass as a batch.
I recommend creating a BOOL and setting them in the -updateConstraints of UIView (or -updateViewConstraints, for UIViewController).
-[UIView updateConstraints]: (apple docs)
Custom views that set up constraints themselves should do so by overriding this method.
Both -updateConstraints and -updateViewConstraints may be called multiple times during a view's lifetime. (Calling setNeedsUpdateConstraints on a view will trigger this to happen, for example.) As a result, you need to make sure to prevent creating and activating duplicate constraints -- either using a BOOL to only perform certain constraint setup only once, or by making sure to deactivate/remove existing constraints before creating & activating new ones.
For example:
- (void)updateConstraints { // for view controllers, use -updateViewConstraints
if (!_hasLoadedConstraints) {
_hasLoadedConstraints = YES;
// create your constraints
}
[super updateConstraints];
}
Cheers to #fresidue in the comments for pointing out that Apple's docs recommend calling super as the last step. If you call super before making changes to some constraints, you may hit a runtime exception (crash).
This should be done in ViewDidLoad, as per WWDC video from Apple and the documentation.
No idea why people recommend updateConstraints. If you do in updateConstraints you will hit issues with NSAutoresizingMaskLayoutConstraint with auto resizing because your views have already taken into account the auto masks. You would need to remove them in updateConstraints to make work.
UpdateConstraints should be for just that, when you need to 'update' them, make changes etc from your initial setup.
Do it in view did layout subviews method
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
I have this solution to change constraints before those who are in the storyboard are loaded.
This solution removes any lags after the view is loaded.
-(void)updateViewConstraints{
dispatch_async(dispatch_get_main_queue(), ^{
//Modify here your Constraint -> Activate the new constraint and deactivate the old one
self.yourContraintA.active = true;
self.yourContraintB.active= false;
//ecc..
});
[super updateViewConstraints]; // This must be the last thing that you do here -> if! ->Crash!
}
You can set them in viewWillLayoutSubviews: too:
override func viewWillLayoutSubviews() {
if(!wasViewLoaded){
wasViewLoaded = true
//update constraint
//also maybe add a subview
}
}
This worked for me:
Swift 4.2
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Modify your constraints in here
...
}
Although honestly I am not sure if it is worth it. It seems a bit slower to load than in viewDidLoad(). I just wanted to move them out of the latter, because it's getting massive.
Add your constraints in viewWillLayoutSubviews() to add constraints programmatically
See Apple Documentation in Custom Layout Section
If possible, use constraints to define all of your layouts. The
resulting layouts are more robust and easier to debug. You should only
override the viewWillLayoutSubviews or layoutSubviews methods when you
need to create a layout that cannot be expressed with constraints
alone.
Following example is to pass any view to another class. create my view from storyboard
Swift 5.0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.abcInstance = ABC(frame: self.myView.frame)
}
}
If you miss DispatchQueue.main.async, it will take time to update constraints in viewWillAppear. Create myView in storyboard and give constraints same as screen width & height, then try printing frame of myView. It will give accurate value in DispatchQueue.main.async or in viewDidAppear but not give accurate value in viewWillAppear without DispatchQueue.main.async.
If I want to initialize views programmatically, where in the viewcontroller lifecycle should this happen?
The initial intuition is loadView. However, here, we don't yet have the frame of the view itself (necessary for calculating the sizes/positions of the views). Ditto for viewDidLoad.
Next intuition is viewWillAppear- here we DO (finally) have a guarantee of the frame of the view. However, this has potential to be called many times throughout the vc lifecycle. Ditto for viewDidAppear, etc...
Finally, I found viewWillLayoutSubviews. This works for the initialization of most static layouts- however, whenever any view moves this gets called again (same problem as viewWillAppear).
I've seen recommendations to init the views in loadView and set their frames in viewWillLayoutSubviews (since setting frames should be idempotent, who cares if it gets called a couple times). But then why does apple so strongly encourage initWithFrame: as the standard initialization method of UIViews (https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/viewpg_iphoneos/CreatingViews/CreatingViews.html)?
Would it be crazy to subclass all my UIViewControllers to have an initWithViewFrame: method? That way I can pass in a frame, manually set it immediately in loadView and be done with it? Or is it better to have a viewHasBeenFormatted flag in viewWillAppear that, if not set, calls the formatting of views and then sets it?
Or is this just apple's way of saying "use interface builder or you're screwed"?
Any help is appreciated!
edit- accidentally wrote loadView where I meant viewWillAppear (in final paragraph)
update- I guess I've come to terms with the fact that there is no place where
The frame is confidently known
The code will only be run once (on setup)
Looks like you're expected to initWithFrame: all your views in viewDidLoad (but then I guess the contents of that view shouldn't treat that frame as even remotely final? because how could it be when it was derived on an assumption? ugh...). Then re-set their frames in layoutSubviews. And make sure to manually handle the differences between initial layout and layout as a result of a moved view there... Man I feel like I've GOT to be missing something... (lol denial...)
I guess that, OR submit and use IB.
update2- viewWillLayoutSubviews WILL get called when one of its subviews is resized. So it is still disqualified as it fails property 2 of the required characteristics that I'm looking for. :(
If you're doing layout with IB, it's fine to do additional view initialization in viewDidLoad (for example, if you need to do stuff that IB doesn't handle well, or if you have UIView subclasses with properties not supported by IB). Alternatively, if you're not using IB, the documentation says you should use loadView to manually initialize your view hierarchy.
You're right, though, that you can't rely on the frame being accurate at that point. So you can accomplish layout via each view's autoResizingMask property, layout constraints (if you're iOS 6 and later), and/or overriding layoutSubviews.
My usual approach is to do layout to some degree in IB, then do anything else I need to (nontrivial layout, custom classes, etc) in viewDidLoad. Then, if I have layout to figure out that autoResizingMask doesn't cover (I'm supporting down to iOS 5), I override viewWillAppear (or layoutSubviews if I'm subclassing UIView) and do some pixel math. I've got a category on UIView to help with this that has things like:
-(void)centerSubviewHorizontally:(UIView *)view pixelsFromTop:(float)pixels;
-(void)centerSubviewHorizontally:(UIView *)view pixelsBelow:(float)pixels siblingView:(UIView *)sibling;
View controllers should not have initWithFrame: methods. What I do in all of my code (I never use IB) is to let the default loadView do its own thing. I create and setup all subviews in viewDidLoad. At this point the view controller's frame has at least a sane value. All subviews can be created with their own sane frames based on the initial size of the view controller's view. With proper autoresizingMask values this may be all you need.
If you need more specific subview layout, put the appropriate layout code in the viewWillLayoutSubviews method. This will deal with any view controller view frame changes including rotation, in-call status bars, etc.
If you don't use interface builder you should override loadView and initialize the views there. If you use autolayout you can also add your constraints there. If you don't use autolayout you can override the layoutSubviews method of your views to adjust the frames.
(iPhone SDK 3.x:) I have a UIControl subclass that creates a different number of subviews depending on the length of an NSArray property. Please take my word for it that this needs to be a UIControl rather than a UIView.
Currently I implement subview management in drawRect, beginning by removing all subviews and then creating the appropriate number based on the property. I don't think this is very good memory management and I'm not sure if drawRect is really the appropriate place to add subviews. Any thoughts on the best way to handle this pattern?
Thank you.
There is a method called layoutSubviews, and like the name already says, that method is thought to layout the subviews. You can call setNeedsLayout and the layoutSubviews method will be called (do not call layoutSubviews directly).