iOS 10, the gift that keeps on breaking, seems to have changed another behavior.
Assume two UIViewControllers are pushed onto a UINavigationController.
On iOS 8/9, calling navigationController?.popViewController(animated: true) to pop the top UIViewController (say VC2) caused viewDidLayoutSubviews on the bottom view controller (say VC1) to get called.
We relied on this to refresh VC1. Sometimes VC2 adds subviews to VC1 (via the data model), and this needs to get reflected when popping back to VC1.
Accurate frame information is required. We can't use viewWillAppear because frame data is wrong on iOS 9. The problem with viewDidAppear is that there is a momentary glitch between seeing the view initially and the adjustment.
Now VC1's viewDidLayoutSubviews does not get invoked when popping VC2.
1) Is this a bug for viewDidLayoutSubviews to not get invoked?
2) What's the right way to refresh view controllers when popping with UINavigationController?
Relying on viewDidLayoutSubviews was never the proper solution. UIViewController provides viewWillAppear: or viewDidAppear: for such a use. When VC2 is popped from the navigation controller, those two methods will be called on VC1 to let you know that is will be or now is visible again.
viewDidLayoutSubviews should only be used to adjust view frames and layout.
viewWill|DidAppear: should be used to handle the view controller becoming visible originally or again. In your case you should use this to update data and add/update views as needed. Those new views should be setup based on the view controller's current frame. They will be adjusted in your implementation of viewDidLayoutSubviews as needed.
I will complement rmaddy's answer. You need to decouple performing layout and updating your data. If your flow is such that data should updated as the view is about to appear, you should update your controller's view-backing data in viewWillAppear:, reload your views and then mark the view as needing layout using setNeedsLayout. This will cause the system to perform a layout on the controller's view, and will trigger the layout. This way, you can ensure the layout is performed once the view is ready, and not before (as often the case is in viewWillAppear:.
Related
If I have multiple view controllers being presented and dismissed in any order, can I be sure that iOS calls viewWillAppear methods in the right order (i.e. order of appearance)?
I cannot find any specific information about this in the documentation.
I think this is all you need to know about viewWillAppear from the docs:
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view. You can override this method to perform custom tasks associated with displaying the view. For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented. If you override this method, you must call super at some point in your implementation.
Only thing that comes to mind that might not be absolutely clear is that this callback is called on the presenting view controller when presented view controller is going to be dismissed (so presenting view controller is going to appear again).
Therefore if A is a root, A.viewWillAppear will be called before it will appear on the screen. Then, if A presents B, just before B becomes visible, B.viewWillAppear will be called. And when B is being dismissed, A.viewWillAppear will get called again, since its view will appear again.
viewWillAppear() is called the first time the view is displayed and it is also called when the view is displayed again, so it can be called multiple times during the life of the view controller object.
It’s called when the view is about to appear as a result of the user tapping the back button, closing dialog, or when the view controller’s tab is selected in a tab bar controller, or a variety of other reasons. Make sure to call super.viewWillAppear() at some point in the implementation
I was trying to create a UIAlertController on ViewWillAppear, it was giving me the below warning.
"Warning: Attempt to present <UIAlertController: 0x7f8798c15df0> on <ViewController: 0x7f8798f81450> whose view is not in the window hierarchy!".
My Understanding it is ready to show the view to the user in ViewWillAppear, but fairly expensive.
However when i moved the same UIAlertController code to ViewDidAppear it was showing the Alert Msg.
Can you please clarify whey the alert msg is not getting show in ViewWillAppear.
UIAlertController is unlike the typical UIAlertView, which inherited from UIView. Adding a UIView in viewDidLoad, appear, etc. is no problem because you are adding it that view controller's view hierarchy.
Now, with UIAlertController, it's a first class view controller. This means that you should present it the same way you would other view controllers - all the same rules apply.
In viewWillAppear, the view controller isn't done being added to the window's hierarchy, so it's a poor choice to present the alert controller. As you found, viewDidAppear is the way to go here.
viewWillAppear: is not a good location to present another view controller since the current one being presented is not yet in the window hierarchy and is also in a transition animation. You should either use viewDidAppear: or add a slight delay before displaying the alert controller.
Most answers on here cover the topic pretty well, I just wanted to add additional details. It typically is not problematic to add child view controllers in viewWillAppear: or even viewDidLoad:. In fact if you use a container view controller in IB then it will be integrated before either of these are called.
The problem is more so to do specifically with the alert controller. Alert controllers function on a different window hierarchy than your typical UI (think Z axis) and manipulate the UI in a more intensive way than a standard container (since it is not a container).
In order for the alert to properly present it must happen after the current view is displayed (there could be a number of reasons here). This is why presenting it in viewDidAppear: worked (since the current vc's view will be fully live).
The same error may occur if parameters of UIAlertController are nil, such as title, message
We have a MainViewController with a tableView, and it presents a new modalViewController.
The MainViewController is restricted to portrait only, and the modalViewController can rotate.
The problem is in iOS8, that when the modalViewController rotates, the callback method of rotation in iOS8 in MainViewcontroller is called - - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
Thus, the UITableView is getting its data reloaded, which is a behaviour we don't want.
Can we prevent this feature of iOS 8, and not rotate the presenting UIViewController?
So after long days of searching and investigating, I finally came up with a possible solution.
First of all, I can use navigation controller and push the viewController instead of presenting it, but it breaks my code and just isn't so true.
The second thing I can do is not setting constraints. I still can use autolayout, but if I don't set constraints, and let the default constraints to be set, the tableView doesn't get reloaded. of course this is also isn't very smart thing to do, as I have many elements in my viewController.
Finally, I figured out that I can show this "modal" viewController in another UIWindow. I create UIWindow and set the modalViewController as its rootViewController.
I put some example project in git:
https://github.com/OrenRosen/ModalInWindow
Hope it will be helpful.
I did something similar with a navigation controller, that wouldn't rotate unless the top pushed controller does rotate.
In your case check if the main controller is presenting another controller. If it isn't then just reject rotation, otherwise return whatever the presented controller returns for the rotation method.
As for your table view, it shouldn't get reloaded because of rotations.
In iOS 8 the view that rotates when you change the device orientation is the first view added to the UIWindow. So, if you save a reference to it in your presentedController, you can overwrite the shouldAutorotate and supportedInterfaceOrientations values.
What is the difference between viewDidLoad and viewDidAppear? What kind of initialization or custom code goes into those functions?
e.g. presentModalViewController works only when present in viewDidAppear and not on viewDidLoad.
viewDidLoad is called exactly once, when the view controller is first loaded into memory. This is where you want to instantiate any instance variables and build any views that live for the entire lifecycle of this view controller. However, the view is usually not yet visible at this point.
viewDidAppear is called when the view is actually visible, and can be called multiple times during the lifecycle of a View Controller (for instance, when a Modal View Controller is dismissed and the view becomes visible again). This is where you want to perform any layout actions or do any drawing in the UI - for example, presenting a modal view controller. However, anything you do here should be repeatable. It's best not to retain things here, or else you'll get memory leaks if you don't release them when the view disappears.
See: https://developer.apple.com/documentation/uikit/uiviewcontroller
Simply put, you would want to create any controls or arrays in viewDidLoad, where as in viewDidAppear is where you would want to refresh those controls or arrays.
viewDidLoad is called once when the controller is created and viewDidAppear is called each time the view, well, DID appear. So say you have a modal view that you present, when that view is dismissed, viewDidAppear will be called, and viewDidLoad will not be called.
When I transition from one view controller (let's call it MasterViewController) to another (called DetailViewController), in what order are the viewWillDisappear:, viewWillAppear:, etc. methods on each controller called?
I suspect some of the cleanup code called when my master view disappears is interfering with the initialization code in my detail view. I've looked through Apple's documentation but can't find any information involving multiple view controllers like this.
I created a simple UINavigationController-based project and added some NSLog statements to find out what order they get called in.
Master prepareForSegue:
Detail viewDidLoad
Master viewWillDisappear:
Detail viewWillAppear:
The new view is displayed (with or without animation)
Master viewDidDisappear: (after animation is finished)
Detail viewDidAppear:
However, when switching between views using a UITabViewController, the order is different:
SecondTab viewDidLoad
SecondTab viewWillAppear:
FirstTab viewWillDisappear:
The new view is displayed.
FirstTab viewDidDisappear:
SecondTab viewDidAppear:
So it seems that you cannot always count on these events occuring in the same order - it can vary depending on the nature of the view controllers you are transitioning between.
Are there any important points that I missed here?