I have a vertical UIScrollView with several 'cards', nested UIViewControllers (via container views), each with their own state. Some of these cards can be expanded to take up the entire screen (what I'd effectively consider a full view controller transition) and later collapsed back to its original size. Ideally, we'd be segueing into these views, but apparently, transitioning into an already displayed view controller triggers an assertion. I'm currently experimenting with just changing the parent UIScrollView's contentOffset and any given card's bounds property upon expand/collapse, but this is proving to be a bit of a p.i.t.a. from several angles.
Is there any way to utilize a proper UIViewController transition for this?
Is there any way to utilize a proper UIViewController transition for this?
Consider instantiating a new instance of the same type of view controller, configuring it with the same data, and transitioning to that instead. That way you don't have to mess around with removing the controller from the container and putting it back later.
Related
(I have read other questions and answers on this topic, but most are very old and do not relate to iOS 9 or 10.)
The app design calls for the top half of the display to always contain the same content. (An image being edited by the user.)
The bottom half of the display needs a UITableView. When a UITableViewCell is tapped, the bottom section needs to transition to a new UIViewController with slide-on animation, similar to how UINavigationController push segues work.
Problem: only the bottom view needs to transition to the new view controller(s), and back again. The upper half of the view hierarchy needs to remain unaffected. For this reason, I can't place everything inside a UINavigationController, and I can't have a UINavigationBar at the top of the screen.
Question: what approach should I take in such a situation, where I need only one UIView hierarchy to transition in push-segue fashion, but not anything else? Thanks.
Edited with Solution
Solution follows, for those following along at home.
Yes, you can actually use a UINavigationController for the bottom half.
If you are using Storyboards, the easiest way to do this is to use a container view for each part of the screen which you then can embed a UIViewController in for the top part and a UINavigationController in for the bottom part. If you are doing this programmatically, just add the view controllers as child view controllers to your app's initial view controller (see this answer for more info) which is essentially what the Storyboard will do for you automatically when using a container view.
As a child view controller, the UINavigationController will act independently from the top UIViewController and should behave as expected.
I recommend the programatic approach for the following reasons:
It helps you understand the inner workings of child/parent view controllers much better which will likely save you a significant amount of debugging time down the line.
It makes adding/removing/swapping child view controllers as simple as a few lines of code. Trying to do this with Storyboards is notoriously hacky and cumbersome.
It's much easier to keep track of changes using GIT (most mid-size/larger companies actually prohibit Storyboards for this very reason)
If you want change in part of the screen you can use container view. For details refer Swift - How to link two view controllers into one container view and switch between them using segmented control?
You can use multiple view in one view controller and can give animation like push or pop to show or hide it.
Second approach is you can use Container View which will give exact effect like navigation stack.
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.
All,
I am running into some performance/memory issues when using several ChildViewControllers in my ParentViewController. Here is my situation: I have a ParentViewController with a dynamic number of ChildViewControllers - some times as many as 20. They are contained in a UIScrollView, and are paged through. I'm running into problems when I have several on the page (I'm only loading the first two, then building the others as I swipe), however, having that many within the ParentViewController is starting to cause some crashes due to memory.
The ChildViewController has a lot going on in it, and I'm going through it to make sure it's as efficient as possible, however, I have concerns about this approach working on older devices (as is, I'm getting crashes on the 5S).
It seems that it would help to change the view controllers to just be views, but it'd be a pretty large endeavor as the VC is complex. One suggestion I had was to create a view from the existing view controller's view, and set several delegate methods on the view and interact with the views from the ParentViewController that way. Does any one have any thoughts on the efficiency of that method as a opposed to the current method of using ChildViewControllers?
Another thought I had was to build a custom ContainerViewController and have all the children in there to swipe through, but I wasn't sure if that would give me an advantage over using the children in a UIScrollView.
Any thoughts?
I personally would not advocate the refactoring of your code to use views rather than view controllers. The view controller, itself, is unlikely to be the source of the memory problems, but rather the model objects they keep track of (as well as the assets the view controller's view uses). I think the key is to simply remove the view controllers (and their views) as they scroll off of the screen.
In your scrolling logic, as you're adding child view controllers that scroll into view, you are presumably doing all of the appropriate containment calls:
UIViewController *newChildViewController = ...
[self addChildViewController:newChildViewController];
newChildViewController.view.frame = ...;
[self.scrollView addSubview:newChildViewController.view];
[newChildViewController didMoveToParentViewController:self];
(See WWDC 2011 video Implementing UIViewController Containment for a discussion about why it's important to do these containment calls, namely to keep your view controller hierarchy synchronized with your view hierarchy.)
As the child views scroll out of view, you just do the appropriate containment calls to remove the child controller (and its view):
[childViewControllerToRemove willMoveToParentViewController:nil];
[childViewControllerToRemove.view removeFromSuperview];
[childViewControllerToRemove removeFromParentViewController];
// also remove any other strong references you have to that childViewControllerToRemove
Alternatively, you might want to contemplate using a UIPageViewController which (in iOS 6+) offers scrolling page view (UIPageViewControllerTransitionStyleScroll) for the transitionStyle. This simplifies the amount of custom container code you have to write to handle the view controllers that scroll in and out of view. The UIPageViewController is designed precisely for this situation of scrolling (or paging) through a bunch of different view controllers' views. See the Page View Controllers discussion in the View Controller Catalog for iOS.
I don't think moving them all to UIView's will help. You could achieve this same effect by just adding aChildViewController.view without ever technically adding it as a childView. I would optimize the loading of the views into your UIScrollView. Make sure you only have, say 4 views loaded into memory at any one time. Another option would be to use a horizontal UITableView or UICollectionView so you can reap the memory management features they have build in.
You could turn it into a tableview and let the cell dequeue take over. You would only have a few in memory at a time then.
I’d like to create a small overlay view that would float on top of all the others. I need the view to accept touches and I need it to stay in place even during transitions between the underlying views. What I have tried so far:
Standalone view inserted into the key UIWindow. The obvious downside is that the view is not attached to any view controller, which means handling (at least?) rotation events by hand.
UIViewController containment, have a “root” view controller that would contain the floating view and all navigation would be done underneath it. This is very much a “first-class-citizen” solution, but presenting a modal view controller overlaps the floating view.
Second UIWindow with a root view controller containing the floating view. This goes against Apple recommendations and I had some trouble filtering which events should go to which window.
Does anyone have a working solution that doesn’t require too much brittle hacking?
In the end I have followed Jonathan’s suggestion and implemented the overlay as a separate view inserted into the key window. The view has an associated controller object (a subclass of NSObject, not UIViewController), and this controller takes care of the rotation logic. That way the view implementing the actual overlay object doesn’t need to care about rotation events at all.
When using custom container view controller, I don't quite understand why the presenting view controller needs to specify the from, because being the container class, it should already know what's in the view hierarchy, no?
transitionFromViewController:toViewController:duration:options:animations:completion:
Container view controllers sometimes need to put the views of the contained controllers into specific subviews of their own view. (For example, a SplitViewController reimplementation might have left and right positioning views holding the master and detail controller views respectively.) Providing the fromViewController tells UIViewController where in the view hierarchy the new controller's view should be inserted, and also which specific view should be removed after the animation.
(contrary to another answer, the frames of the views aren't set for you at all. You do that, before the call, and in the animation block. The "Creating Custom Container View Controllers" system guide in the docs has a pretty good example.)
As it happens, actually using transitionFromViewController:... appears to be optional. You can manage your view hierarchy manually, with or without animations, and it works fine. I'm still forming my opinions, but I think I prefer to do it manually, to more easily handle cases where one of the VCs is nil.
This is done this way to allow you to have a view controller that has views with viewControllers in it. The from defines the originating view controller and gives the system the ability to position the animations appropriately.
Imaging you had a view with 4 views in it like tiles. the main view controller can consecutively call this on its "child" view controllers and with the from -> to specification, it won't make the assumption that the caller is the from viewController.