I prefer to disable autoresizesSubviews and to use setFrame to place all of my subviews.
As of iOS 6, things seem to have changed a lot.
When I call setFrame on a view in viewDidLoad, there is no effect. I tried it from viewWillAppear; same thing.
A setFrame call will work in willAnimateRotationToInterfaceOrientation, but that is not called initially like it was in iOS 5.
Can someone clarify please where I am expected to layout my views from?
I guess you want to layout your views from UIViewController. Have you tried performing your layout tasks in viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
According to Apple's documentation:
Your view controller can override this method to make changes after the view lays out its subviews.
Since your view itself does not do layout its subviews (you do not use autosize, autolayout or layoutSubViews:) you can do the layout tasks in this method.
Nevertheless, the elegant way would be to use a custom parent UIView and perform all the layout there, overriding UIView's layoutSubViews: (unless you add/removes views dinamically). Quote from Apple's documentation on "How View Controllers Participate in the View Layout Process":
Ideally, the views themselves perform all of the necessary work to reposition themselves, without requiring the view controller to participate in the process at all. However, if the view controller adds and removes views dynamically, a static layout in Interface Builder may not be possible. In this case, the view controller is a good place to control the process, because often the views themselves only have a limited picture of the other views in the scene.
You can use setFrame in viewDidLoad if you uncheck the Autolayout option in Interface Builder. If you need to use auto layout, You need to perform your layout tasks in viewDidLayoutSubviews:.
Regarding "Asking the views to lay themselves out doesn't make sense to me. Views don't have authority to say where they should go. That's the role of the controller, isn't it?". I think the answer is often negative. It's the parent view (not view controller) which usually plays the role of laying out it's own subviews. It does so by utilizing autoresizingMask or autolayout (iOS 6 only). Or You can make layout programmatically by overriding -layoutSubviews method of the parent view.
Sure, like Imre mentioned, controller can participate in the layout process as well by overriding - (void)viewDidLayoutSubviews. I often use this approach when I don't want to subclass the top level view of the view controller for keep things simple.
Finally, I found this post super useful. Even with some minor errors, the post provides a big picture of view layout process.
Related
I'm trying to create a custom container view controller. I want to use the standard ViewControllerTransitioning pattern for animating the addition of child views, but I'm a bit stuck. I'm writing this container view controller completely in code, as the number of child view controllers can vary.
I've already looked at this tutorial and it has helped me very much, but I can't figure out this step.
The flow of adding a new child view controller is currently like this:
'MyContainerVc'.addNewSubViewController
Call UIViewController.addChildViewController
Create transitioning context
Fetch animator
Animate
'Container View'.addSubview
Create autolayout positioning/height constraints
The positioning/height constraints can be created (and are) in the container view, as the container view controller does not need to control them, but the width constraint needs to be created in the container view controller, as the child vcs can specify a preferredContentSize.width to my container vc. The container vc will try to respect the child vcs' preferences as much as possible and base the width constraints on it as much as possible.
I'm not sure when the constraint for the width of the subviews can be created. The constraint needs to be enforced before the animation is performed, as the width should be defined before the child view appears. However, I cannot activate it before the child view has been added as a subview and if I activate it after the animation has completed, the width is not defined properly during the animation.
The animator should only use the transitioningContext and use the addSubview method of the container view, as it should be agnostic to what kind of transition it is animating.
Also, is creating the AutoLayout constraints in addSubview the best way to go or should it be done somewhere else, e.g. in didAddSubview?
Some advice is very appreciated!
I think I figured it out. I'm going to create a delegate protocol that specifies a function that tells the container view controller to create the width constraint and I'll call that method in addSubview of the container view.
It appears that presenting and dismissing a view controller both prompt the presenting view to layout its subviews and/or update its constraints. With a heavy view hierarchy, this is introducing performance issues. Again - this is the existing, currently displayed view. The modal being created and displayed is very light.
This occurs whether I use autolayout (as in my example project) or not.
I have built a demo project that approximates an app I am working on. There is a main parent controller with a horizontally scrolling UIScrollView. Multiple child controllers are added to the parent controller, and their views are added to the scrollview and arranged using NSLayoutConstraints. Each child view has one subview itself, a simple UIView, also arranged with a constraint.
In the navigation bar, there is a button to launch a modal. When presented, the parent controller makes a call to setNeedsLayout on each child view, multiple times. In my demo project, I am overriding setNeedsLayout to log when it is accessed. The same occurs when closing the modal. Open and close the modal a few times and observe the console.
I can see no reason why a new layout is needed, and with more complex views I am finding that hundreds of these calls are firing, with a noticeable performance impact.
Note that when the layout code from ChildView is omitted, setNeedsLayout is not called. I encourage you to comment out the constraints and see the difference in the logging.
Why is this happening? How can I prevent an unnecessary layout pass when presenting and dismissing a modal?
First of all, you are logging setNeedsLayout, which is just a flagging mechanism and does not really incur any work yet. Multiple calls to setNeedsLayout may only trigger a single layout. You should be logging -[UIView layoutSubviews] or -[UIViewController viewDidLayoutSubviews] instead, because these are where the actual heavy-lifting happen.
Second, layout-related methods are meant to be called repeatedly and rapidly during presentations because:
The window needs to rotate all its subviews to respect the presented view controller's preferred interface orientation.
Animations will need to know the initial and final states of your views.
When layouts happen on parent views for whatever reason, all their subviews (which may include views of your view controllers) will of course need to update their layouts too.
If you want to minimize the number of layout passes, you can try give up using presentViewController:animated: and instead use addChildViewController: and animate just the necessary views manually. But even then, you may still trigger the parent controller's layout anyway.
You are doing a very, very, very odd thing: you're maintaining a custom parent view controller with 10 child view controllers all of whose views are in the interface simultaneously. View controllers are not designed for that sort of thing. It is this that is triggering the multiple layoutSubviews calls that you are seeing. It is fine to have multiple child view controllers, but their views should not all be in the hierarchy - especially in your case, where only one such child view is actually visible.
In fact, the interface that you've constructed - a paging scroll view, each of whose "pages" is a view managed by a view controller - is already implemented for you by UIPageViewController, which is far more efficient, as it only actually maintains at most three view controllers at a time: the view controller managing the visible view within the scroll view, and the view controllers managing the views to its right and left. It is also wonderfully convenient and easy to use.
So either:
You should use UIPageViewController, or
You should imitate what UIPageViewController does, removing view controllers' views (and perhaps even releasing the view controllers) when they have scrolled out of sight - as we had to do in the days before UIPageViewController existed - see the Advanced Scroll View Techniques video from WWDC 2011. My Latin "flashcard" app worked this way before UIPageViewController came along; it has thousands of vocabulary cards, each of which is managed by a view controller, but only a maximum of three card view controllers ever exist at any one moment.
(By the way, you should also not be using your own self.childControllers mutable array, as this list is already maintained for you as self.childViewControllers.)
I think layoutSubviews is getting called because the presenting controller's view changes superviews while animating out of the screen once hidden by the presented view.
If you want to avid skip layoutSubviews when the frame hasn't changed, just save a reference to the last frame and if equal return without doing anything. Also there is no need to call setNeedslayout on subviews as the system will trigger it automatically if you resize them.
Anyway, your main problem is your approach:
Only use view controllers if you're going to use them as such (inside a tab bar controller, pushed to a navigation controller, as a window's rootController, presented modally, etc.). If you want to manually add views do not use view controllers and just use custom views! This is a very common error and you can see more details here.
Load views and objects lazily and reuse them. For instance you should only load 1~3 pages of contents and load new ones only when the user scrolls to them. When loading a new one remove one of the old views, or better yet reuse it.
You can separate the logic not only with controllers but also with custom views. Some reasons why you should not use controllers in your particular case:
Controllers won't get retained by a container controller or window as you're manually adding their views.
Controllers won't get orientation, memory, viewDidAppear, etc., events. Again because you're not using them as proper view controllers.
If you properly implemented a custom container controller (which is a lot of work to do properly), then you could use controllers. Otherwise stick to custom views.
In my view I am using a UITableView that is controlled by a UITableViewController on the top half of the screen. The remaining screen is used for a UIScrollView that contains a view that is controlled by the main UIViewController.
When I perform a pull down to refresh in the UITableViewController, (for some reason if the number of table entries is less than or greater than the initial load value, the UIScrollView in the main UIViewController's frame gets changed to the screensize...
Essentially it breaks my paging unless I reset the scrollview back to the intialized size...
I have no idea why this happens as the UIScrollView is not used in the UITableViewController. The only scrollview that is used in the UITableViewController is the UITableView's to handle pull down to refresh...
Does anyone know why the main UIScrollView's contentSize gets changed randomly when it shouldn't even been accessible from the UITableViewController class?
Thanks
Just tried it here, and I can't duplicate your experience. I'm guessing you have an unexpected or inconsistent view/controller hierarchy? Look at the controller of the table and scroll views' common superview. Anything fishy there? Remember: view controllers manage sets of views. Container view controllers manage other view controllers and have special rules (see: The View Controller Programming Guide, esp. -addChildViewController:, etc.).
I'd suggest opening a blank project and trying to recreate the problem in its simplest possible form. If it's magically fixed, what's different? If it's still giving you trouble, send us a link so we can see the details of how you have things wired.
What is the best practice for implementing a horizontal scroll view with paging, with one view controller per page?
Is the PageControl example still the best way to implement this now that iOS5 has API for view controller containers/containment?
I know this question is a little old, but as of iOS 6, UIPageViewController has a new transition style property called UIPageViewControllerTransitionStyleScroll which lets you use the page controller for a use case like yours, with simple scrolling between pages instead of the iBooks-style page curl transitions.
You should also watch the 2012 WWDC video number 223 - Enhancing User Experience with Scroll Views where they basically transition an old app that uses the method you describe to the new UIPageViewController with scroll style transition.
My answer would be that it depends on your goal. If you want to make an app as efficiently as possible, I would just use the way it is done in the sample code. I have used it before, and I would do it again.
On the other hand, if your goal is to learn about view controller containment, how it works and how to use it, this might be a good case to try it out. If you go that way, don't forget to check out the WWDC video "Implementing UIViewController Containment" (https://developer.apple.com/videos/wwdc/2011/).
Appears best practice as of iOS5 is to remain using the same method as the PageControl example. That is one controller class (note not a view controller). With child view controllers for each page.
As of now there is no documented better way to implement a paging scroll view using view controller containment methods included in iOS5.
I would suggest a UIPageViewController instead.
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIPageViewControllerClassReferenceClassRef/UIPageViewControllerClassReference.html
I think UIPageViewController can go horizontally:
- (id)initWithTransitionStyle:(UIPageViewControllerTransitionStyle)style navigationOrientation:(UIPageViewControllerNavigationOrientation)navigationOrientation options:(NSDictionary *)options
And
enum {
UIPageViewControllerNavigationOrientationHorizontal = 0,
UIPageViewControllerNavigationOrientationVertical = 1
};
typedef NSInteger UIPageViewControllerNavigationOrientation;
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIPageViewControllerClassReferenceClassRef/UIPageViewControllerClassReference.html
If you add your View Controllers views (myViewControllerOne.view) to the UIScrollView - and then appropriately frame them to appear horizontally - when the user interacts with that view inside of the scroll view methods in the View Controller will be called.
By this I mean if you have MyViewControllerOne.view as a sub view inside of your scroll view, when you scroll to that 'page' of the scroll view and press the button the attached method (IBAction etc) in MyViewControllerOne will be called.
This should give you all the functionality you need such as adding UI Elements, tables, another scroll view, etc.. These elements will be controlled from their originating View Controller.
Hope this helps!
In my custom view controller I override loadView to programmatically set up my view hierarchy. To support auto-rotation I assign an appropriate value to the autoresizingMask property of all views in the hierarchy - except for the root view (the one that I assign to self.view). Despite this omission, resizing on auto-rotation works perfectly (it feels so good to be finally able to say this ☺). I have tested in various simulators, and also on my iPhone and iPad devices.
Does anyone know why this is the case? Is this behaviour just "luck", or is it officially documented by Apple somewhere?
(Note: I'm asking mostly out of curiosity, please don't tell me to "go fix it, it's just one line of code" - I have already done so.)
autoresizesSubviews/autoresizingMask define a superview-to-subview relationship. In the case of a UIViewController that supports rotation, the controller is managing the view's frame on rotation independent of any autoresize properties.