I started to use AutoLayout in my latest project, and I am stuck at some point. I use a content view controller, which adds other view controllers as sub view controllers, and their views as subviews aswell. Everything is fine, except that my sub controllers' view have wrong view sizes.
One of my sub view controllers sets up something like a slider, which has a knob. The initial position of this know depends from the actual size of this view, which is not fix, because I reuse it in many forms. Everything is set up with AutoLayout. Of course, because the value of this slider is coming from another part of code, the position of the know can be described only something like this:
self.knobVerticalPositionConstraint.constant = topPos + height * percentage;
where height is depending on the size of the slider view itself.
The problem is, that when the whole thing gets displayed, parts of my UI have wrong sizes. I even have a wrong view size in the sub controller's -viewDidAppear.
I think this is a problem of how I wrote my code. Maybe not everything is in it's place. I have read this article, which promises a clean and elegant way to solve this type of issues, but it is never revealed:
http://doing-it-wrong.mikeweller.com/2013/06/ios-app-architecture-and-tdd-1.html
I have read the whole Internet, but I didn't found anything like this. It is full of stuff for beginners and first step guides about "How to use AutoLayout", but I never found any advanced tips.
So my specific question is: do you have any best practices, how to assemble a View Controller–View pair which uses AutoLayout from code? Where do you set up your constraints? Where do you update constraint constants like mine, which depends on the calculated values of the constraint equalation system? What code goes to the View and what to the controller? I'd be glad if you could share me some tips to solve this issue.
I have recently completed the migration of my first (and so far only) iOS project to Auto Layout. The stuff below is roughly what I have taken away from my efforts. It's not much, but since I wanted to write it down for myself anyway, here you go, maybe it helps.
Static constraints that never change go into a view controller's loadView override, or in a view's initializer (or some sub-method thereof)
Dynamic constraints which depend on other values calculated by the layout system go into a view controller's viewDidLayoutSubviews override, or in a view's layoutSubviews override (in the latter case it makes sense to call [super layoutSubviews] first to get updated values that the dynamic constraint can be based on).
After a change to a dynamic constraint, it may be necessary to tell the layout system that something changed. For instance, in a view's layoutSubviews override I call [super layoutSubviews] a second time after the constraint change so that the update is effective immediately
A view controller can also override updateViewConstraints, and a view can override updateConstraints, but I have not used these so far.
There are many other events that might trigger an update of dynamic constraints (e.g. keyboard events), but this is beyond the scope of best practices.
Some general guidelines:
Constraints are installed by the view controller or view who sets up the view hierarchy and therefore knows about and is responsible for the layout of the view hierarchy. Although this rule is very basic, it sometimes pays to keep it in mind.
The translatesAutoresizingMaskIntoConstraints property of a UIView is set by that view controller which installs constraints for that UIView
A view controller therefore does not set the translatesAutoresizingMaskIntoConstraints property of its own root view. Instead, this is the responsibility of the parent view controller.
View controller properties related to bar handling in iOS 7:
edgesForExtendedLayout, usually set to UIRectEdgeNone
topLayoutGuide and bottomLayoutGuide
make all bars opaque (by default all bars are translucent in iOS 7), which would then allow the property extendedLayoutIncludesOpaqueBars to kick in
And some traps:
Don't install constraints in UIWindow. When I tried this I had trouble when dismissing a VC after it was presented modally.
The view controller properties topLayoutGuide and bottomLayoutGuide should only be used inside a view controller's override of viewDidLayoutSubviews, and after they are used to install constraints one must invoke [self.view layoutSubviews]. This advice is actually from Apple's documentation of those two properties. Not following the advice may lead to weird layout issues (it happened to me).
If a view controller's view hierarchy contains one or more scroll views (e.g. table views), and there are unexpected layout results, then the view controller property automaticallyAdjustsScrollViewInsets is probably playing a role. Setting it to NO might help.
My personal favourite ressource, by the way, for understanding Auto Layout was and still is this section from the book "Programming iOS 6".
Related
I inherited an overly complicated project (so I don't know all of the inner workings), and I'm running into a bug. Certain parts of my app have some long animations done with CATransaction, and it seems to be causing layoutSubviews to be called repeatedly while the animations are active. This doesn't happen on ios5 and everything looks correct, but on ios6 it gets called nonstop and interferes with a lot of the layout of the view. The stack trace is all hidden/grayed out, but it does seem to begin with CA::Transaction::commit()
Did anything with CATransaction change between ios versions to cause something like this?
See this post: UIView/CALayer: Transform triggers layoutSubviews in superview
Apple answered me via TSI:
why am I seeing this behavior?
is this inconsistency or am I misunderstanding some core concepts?
A view will be flagged for layout whenever the system feels something has changed that requires the view to re-calculate the frames of its subviews. This may occur more often than you'd expect and exactly when the system chooses to flag a view as requiring layout is an implementation detail.
why does it cascade upwards the view hierarchy?
Generally, changing a geometric property of a view (or layer) will trigger a cascade of layout invalidations up the view hierarchy because parent views may have Auto Layout constraints involving the modified child. Note that Auto Layout is active in some form regardless of whether you have explicitly enabled it.
how can I avoid superview to layoutSubviews every time I'm changing transform?
There is no way to bypass this behavior. It's part of UIKit's internal bookkeeping that is required to keep the view hierarchy consistent.
Sounds like an Autolayout issue. Does the view or any of its subviews use Autolayout? Autolayout is nice but doesn't seem very fast and efficient so may cause issues when animating.
Of course it may be necessary for the subviews to be layed out each step in the animation if the size one shape of the view is changing in a way that affects subview placement or size. Consider the animation and what effects it has.
After upgrading my project to iOS 6, I realized that auto layout is only effective in viewDidAppear and most of my code expects the view's frame to be available in viewDidLoad. This limitation renders the really nice auto layout feature almost useless for me. Is there any suggestions to help me use auto layout?
For example, sometimes the developer needs to adjust information about a subview based on where auto layout chooses to place that particular subview. The subview's final location cannot be ascertained by the developer until AFTER the user has already seen it. The user should not see these information adjustments but be presented the final results all at once.
More specifically: What if I want to change an image in a view based on where auto-layout places that view? I cannot query that location and then change the image without the user seeing that happen.
As a general rule, the views frame/bounds should never be relied on in viewDidLoad.
The viewDidLoad method only gets called once the view has been created either programmatically or via a .nib/.xib file. At this point, the view has not been setup, only loaded into memory.
You should always do your view layout in either viewWillAppear or viewDidAppear as these methods are called once the view has been prepared for presentation.
As a test, if you simply NSLog(#"frame: %#", NSStringFromCGRect(self.view.frame)); in both your viewDidLoad and viewWillAppear methods, you will see that only the latter method returns the actual view size in relation to any other elements wrapped around your view (such as UINavigationBar and UITabBar).
As told by #charshep in a comment, calling view.layoutIfNeeded() in viewWillAppear can do the trick.
Quote of his original comment
I had trouble getting a table view to appear at the correct scroll position when pushing it [...] because layout wasn't occurring until after viewWillAppear. That meant the scroll calculation was occurring before the correct size was set so the result was off. What worked for me was calling layoutIfNeeded followed by the code to set the scroll position in viewWillAppear.
XCode 4.2 (the only version of XCode I've ever used) creates a ViewController with a default View property.
I cannot see the code for this default View so I cannot draw on it (because I cannot access drawRect). Is that correct?
Assuming answer to the above is that I cannot draw. If I want to draw on the window should I:
Add a subview to the default View and then draw (via drawRect) on the subview? or
Replace the default View with one that I create and can therefore draw on via drawRect?
This is not a question of how to do this, I want to know what would be the best way. Thanks.
If you need to do custom drawing in you view, then you should define your own custom UIView class, where you override drawRect to do the custom drawing.
Now, wether you follow either approach 1. or 2. depends on what your app does. Both are perfectly reasonable.
Say, for example, that you would like to do your custom drawing, but also show some kind of controls (e.g. to clear the drawing, to reset it, and so on), then I think it could be a better design if you have a root view that you add both the custom view and the other view to as subviews. But this is just an example and it depends on what you are trying to accomplish.
Hope this helps a bit...
I'd love to see a detailed explanation on how to manage views programmatically. I'll provide an overview of how I'm doing it now and would like either comments on how my approach sucks or just an overview of how to do it cleanly and properly.
Basically, in my app's main view controller's loadView method, I first create a root view and set self.view to it. When I want to attach a controller, say, the first one that displays, I call this method:
-(void) attachViewForController:(UIViewController*)controller
{
[self.mRootView addSubview:controller.view];
[controller viewWillAppear:NO];
}
Notice that I explicitly call viewWillAppear (I believe it wasn't automatically calling it), where I manually perform any animations to bring the view in (sliding in, fading in, etc). Is there anything wrong or strange with this approach here?
Now, when I want to leave this view and switch to another, I call a method to setup the switch:
-(void) setControllerSwitch:(UIViewController*)outgoingController
incomingController:(UIViewController*)incomingController
delay:(float)delay;
{
self.mOutgoingController = outgoingController;
self.mIncomingController = incomingController;
self.mSwitchControllerTimer = [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:#selector(switchControllerCallback) userInfo:nil repeats:NO];
}
At this moment in time, I've begun the exit animations for the outgoing view, and this method records the controllers and schedules a method that will perform the actual switch at the moment the outgoing view is done animating. Like this:
-(void) switchControllerCallback
{
self.mSwitchControllerTimer = nil;
// remove outgoing view
[mOutgoingController.view removeFromSuperview];
// add incoming view
[self attachViewForController:mIncomingController];
}
Is this a decent way to manage things? A few points:
I know I could probably instead setup a callback to trigger when the outgoing controllers animations end, but chose to just do it via an explicit delay param to leave room to allow me to cross fade views. However, I think calling setControllerSwitch early may in fact not allow crossfading because it would junk the old controller early and make it chop off its animation.
As mentioned earlier, I'm curious to know if explicitly calling viewWillAppear is a no no and there is a more appropriate way to manage view flow.
Copied from Apple developer documentation but it help me lot to understand about views and maneging multiple views.
Tips for Using Views Effectively
Custom views are useful for situations where you need to draw something the standard system views do not provide, but it is your responsibility to ensure that the performance of your views is good enough. UIKit does everything it can to optimize view-related behaviors and help you achieve good performance in your custom views. However, you can help UIKit in this aspect by considering the following tips.
Views Do Not Always Have a Corresponding View Controller
There is rarely a one-to-one relationship between individual views and view controllers in your application. The job of a view controller is to manage a view hierarchy, which often consists of more than one view used to implement some self-contained feature. For iPhone applications, each view hierarchy typically fills the entire screen, although for iPad applications a view hierarchy may fill only part of the screen.
As you design your application’s user interface, it is important to consider the role that view controllers will play. View controllers provide a lot of important behaviors, such as coordinating the presentation of views on the screen, coordinating the removal of those views from the screen, releasing memory in response to low-memory warnings, and rotating views in response to interface orientation changes. Circumventing these behaviors could cause your application to behave incorrectly or in unexpected ways.
For more information view controllers and their role in applications, see View Controller Programming Guide for iOS.
Minimize Custom Drawing
Although custom drawing is necessary at times, it is also something you should avoid whenever possible. The only time you should truly do any custom drawing is when the existing system view classes do not provide the appearance or capabilities that you need. Any time your content can be assembled with a combination of existing views, your best bet is to combine those view objects into a custom view hierarchy.
Take Advantage of Content Modes
Content modes minimize the amount of time spent redrawing your views. By default, views use the UIViewContentModeScaleToFill content mode, which scales the view’s existing contents to fit the view’s frame rectangle. You can change this mode as needed to adjust your content differently, but you should avoid using the UIViewContentModeRedraw content mode if you can. Regardless of which content mode is in effect, you can always force your view to redraw its contents by calling setNeedsDisplay or setNeedsDisplayInRect:.
Declare Views as Opaque Whenever Possible
UIKit uses the opaque property of each view to determine whether the view can optimize compositing operations. Setting the value of this property to YES for a custom view tells UIKit that it does not need to render any content behind your view. Less rendering can lead to increased performance for your drawing code and is generally encouraged. Of course, if you set the opaque property to YES, your view must fills its bounds rectangle completely with fully opaque content.
Adjust Your View’s Drawing Behavior When Scrolling
Scrolling can incur numerous view updates in a short amount of time. If your view’s drawing code is not tuned appropriately, scrolling performance for your view could be sluggish. Rather than trying to ensure that your view’s content is pristine at all times, consider changing your view’s behavior when a scrolling operation begins. For example, you can reduce the quality of your rendered content temporarily or change the content mode while a scroll is in progress. When scrolling stops, you can then return your view to its previous state and update the contents as needed.
Do Not Customize Controls by Embedding Subviews
Although it is technically possible to add subviews to the standard system controls—objects that inherit from UIControl—you should never customize them in this way. Controls that support customizations do so through explicit and well-documented interfaces in the control class itself. For example, the UIButton class contains methods for setting the title and background images for the button. Using the defined customization points means that your code will always work correctly. Circumventing these methods, by embedding a custom image view or label inside the button, might cause your application to behave incorrectly now or at some point in the future if the button’s implementation changes.
I've got a fairly complex view, for me anyway, that has a few "trays" with custom interface items on them. They slide in and out of my root view. I'd like to nest (addSubview) the items inside the view. Each needs some setup before being displayed...and none can be configured in IB (they're subclasses of UIView).
I'm wondering if it makes sense to subclass UIViewController for each "tray" and then have the VC's view property point to the "tray" view which I can populate with my custom UIView objects. This way I can leverage the viewDidLoad, etc... methods in UIViewController.
I'm not aware of others doing this - at least in the few samples I've looked at. It would create a situation where there would be multiple view controllers being displayed on the screen at once. from the Navigation controller itself on down to the rootViewController and its view and then any number (well, screen size permitting) of these small trayViewControllers. If so, how's the responder chain work? i assume it'd go from lowest UIView to its enclosing VC, then to that VC's parent view, then that view's VC, etc. etc. repeat, repeat.. up to UIApplication... am I asking for trouble?
OR, do I just stick with UIViews and adding subviews into subviews, etc. etc..
Prior to iOS 5.0 this will specifically not recommended because the nested view controllers' lifecycle events – viewWillAppear, etc. – won't be called. See Abusing UIViewControllers.
With multiple UIViewController’s views visible at once some of those controllers may not receive important messages like -viewWillAppear: or -didReceiveMemoryWarning. Additionally some of their properties like parentViewController and interfaceOrientation may not be set or updated as expected.
iOS 5.0 added containment UIViewControllers that correctly handles those lifecycle events by adding child view controllers.
- (void)addChildViewController:(UIViewController *)childController
I spent countless hours trying to get nested view controllers to work in iOS 4. I eventually did, but it required a lot of glue code that was easy to get wrong. Then I saw the warning in the docs.
I'm trying to do the same thing, but was dissuaded from your approach by Apple's documentation, which states that "You should not use view controllers to manage views that fill only a part of their window—that is, only part of the area defined by the application content rectangle. If you want to have an interface composed of several smaller views, embed them all in a single root view and manage that view with your view controller."
My experience on what you are trying to do has been a good one. I try to keep nib files as simple as possible, so I take any posible "subview" and encapsulate it in its own nib file with it's own view controller, therefore I end up having nested view controllers.
In one of my apps I have a very complex table view cell, that has a subview. So I ended up having a hierarchy that goes like this: the tableview controller on the top level, the tableviewcell's controllers for each row and inside each of these a subviewcontroller for the subview inside each cell.
And everything works fine.
Pardon my english.