As seen in this stackoverflow answer about
"NSNotificationCenter with respect to ViewWillAppear and
ViewWillDisappear"
We are pointed in the direction of the below as the preferred approach
"Registering the notification in viewWillAppear and unregistering it
in viewWillDisappear seems to be a clean and symmetric solution to
me."
This is approach is also suggested in this other stackoverflow answer.
My Question is twofold
Why does Apple's AVCam Sample Code in "AAPLCameraViewController.m" removeObservers in viewDidDisappear and not viewWillDisappear as the above answers suggested.
Why do they utilise addObservers after [super viewWillAppear:animated];
while they removeObservers before [super viewDidDisappear:animated];
Code Extracted from "AAPLCameraViewController.m"
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.sessionQueue, ^{
switch (self.setupResult) {
case AVCamSetupResultSuccess: {
// Only setup observers and start the session running if setup succeeded.
[self addObservers];
[self.session startRunning];
self.sessionRunning = self.session.isRunning;
break;
}
}
- (void)viewDidDisappear:(BOOL)animated {
dispatch_async(self.sessionQueue, ^{
if (self.setupResult == AVCamSetupResultSuccess) {
[self.session stopRunning];
[self removeObservers];
}
});
[super viewDidDisappear:animated];
}
There is no single rule/way to register for listening to an event and unregister for the same. It totally depends on the requirements and program flow.
As for your questions:
1)Why does Apple's AVCam Sample Code in "AAPLCameraViewController.m"
removeObservers in viewDidDisappear and not viewWillDisappear as the
above answers suggested.
The Apple's sample code basically deals with the user interactions. So it needs to observe the events when user can actually see the view and interact with it. The observer is removed in viewDidDisappear as it is certain at this point of time that the user would not be able to interact with the view anymore. viewWillDisappear defines a state in view-controller's lifecycle that the view is about to be removed but hasn't been removed yet.So, in theory it's better to stop listening to an event when it would not occur anymore(Here viewDidDisappear). Although, the code might work even if the event is unregistered in viewWillDisappear, but , in certain cases, the coder might have put a lot of code on main thread in viewWillDisappear and stopped listening to events. In this case, some events might be missed.
2)Why do they utilise addObservers after [super
viewWillAppear:animated]; while they removeObservers before [super
viewDidDisappear:animated];
The observer is added on viewWillAppear because the event lifecycle for the event is gonna start(User interacting with view). It does not really matter whether event is registered before/after [super viewDidDisappear:animated]. According to the programming architectures followed in objective oriented languages, the cleanup of child classes precedes it's parent/super classes. So, the cleanup(removing observer) in child is done before cleanup in Parent. Adding observer in viewWillAppear and removing in viewDidDisappear ensures that observer is added before event started and ended after the event ended(What the program requires).
What i consider for NSNotificationCenter implementations?
Life-cycle of event that is gonna need observers.
When to register for an event?(Before event is about to start)
When to unregister for an event?(After event has ended).
Any cleanup needed after unregistering events.
**There a few other AVFoundation specific reasons for adding on viewWillAppear, but the explanation above should sum up the crux.
Where you add and remove the observers depends on your needs. In many cases you would want to add the observer in one of the init... methods or viewDidLoad and remove it in dealloc.
Regardless, there should be some sort of symmetry so it is removed once for each time it is added.
There's really no practical difference between adding in viewWillAppear and viewDidAppear and there's really no practical difference between removing in viewWillDisappear and viewDidDisappear. It will be a rare notification where those differences matter. Same for whether the addition or removal is done before or after the call to [super ...].
Apple's example is a good model to follow. Things are setup before the view is displayed and cleaned up after the view is gone. But again, it really depends on what notification you are dealing with and when you want to listen for the notification.
Ask yourself when you need to know about the notification. Is it during the entire lifetime of the view controller? Is it only while it's visible? Is it from just before it's visible to just after it's not?
The answer to that question determines the best methods to add and remove the observer.
A case can be made that viewWillAppear and viewDidDisappear are the balanced pair not viewWillAppear and viewWillDisappear. The thinking is to set up in viewWillAppear, before the view is visible, and to clean up in viewDidDisappear after it's removed. Your code will be in place for the entire duration that the view is visible.
The sample code attempts to create and remove observers when the controller's view is not yet visible to the user (hence the before appear and after disappear) so as to enhance the user's experience (avoid blank screens, for instance, or redraw while the view is visible).
As for the second question, it appears to be mostly a preference. I do the same: a subclass lets the parent initialize first, and de-initialize last.
Related
I am using a UITabBarController, and my 3rd tab observes an array on a singleton data store (implemented in viewDidLoad).
Currently if I just log out (and change root view controller from App Delegate), the app will crash when dealloc is called on that 3rd tab with the message "cannot remove observer for the key path "X" because it is not registered as an observer.
Using breakpoints, I see that viewDidLoad is never called on this 3rd tab, however dealloc is being called when I sign out. What is going on? I assume the UITabBarController is holding a reference to the 3rd tab when I enter the storyboard, but does not "load" that tab. Yet iOS calls dealloc on it when I release the tab bar controller.
Should I use a boolean to track viewDidLoad execution, or try to remove the observer with a #try statement? Is there an overall better design for this?
Do not use #try. Exceptions in Objective-C should always be considered programmer error, and should be fatal.
As you say, use a boolean ivar set in -viewDidLoad to avoid this.
The view has not been loaded because views are only loaded when they are required for display.
Raw KVO can be dangerous and unwieldy. While not required to answer this question, ReactiveCocoa significantly improves the KVO experience.
viewDidLoad is called before the view appears for the first time. UITabBarController is creating the relevant UIViewController, but the view is not loaded during creation. It is loaded on-demand, when a user visits the tab for the first time.
KVO removal is problematic, I don't think you can avoid using #try in dealloc. I would suggest to use KVOController: it's fairly easy to use and it would also handle all the edge cases for you.
May have found an even better solution. I add the observer in the method initWithCoder:(NSCoder *)aDecoder, which is called when the parent UITabController is loaded. I am using the storyboard which may be why I need to call override this method instead of regular init. Doing this now without the need for a BOOL flag or #try and no crashing.
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[anObject addObserver:self forKeyPath:aKeyPath options:0 context:NULL];
}
return self;
}
Use a flag to set whether or not KVO has been set up. Using #try can create memory management issues depending on the state of the app.
Does anyone have an official reference for why methods such as 'viewWillDisappear' should not be called directly? There are several existing posts on the subject but no official link, only opinions.
It does not make sense to do so as it is calling a method "out of cycle" from the lifetime management of a view. They can be, and in many cases are, overridden of course.
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/RespondingtoDisplay-Notifications/RespondingtoDisplay-Notifications.html
The issue in question is for some code I encountered where 'viewWillDisappear' is being called from some method. It is really the content of the 'viewWillDisappear' method that is needed to be called.
Example:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The following two methods are the ones that need to be called below
[self someMethod];
[self anotherMethod];
}
- (void)delegateMethod
{
[self viewWillDisappear:YES];
// Do some other work
// View is moved off-screen, not deallocated, and therefore, does not "disappear"
}
Instinctively it appears wrong to call any of the view hierarchy methods directly (e.g. viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear). If you tell 'self' and the 'super' view that viewWillDisappear it may do something in the framework that could cause problems later on. I think these methods should be called by the framework only. However, this is my opinion and not an official source. The header files don't seem to provide anything about this.
Any help appreciated.
There is no technical reason you can't.
But you shouldn't.
It's bad coding practice and you might confuse the reader or, worse, make him/her not trust you.
Good coding practice would be the following:
- (void)delegateMethod
{
[self doCommonWork];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self doCommonWork];
}
- (void)doCommonWork
{
// …
}
I every time use dealloc for remove observer, but just faced with this link that describes that we can use viewWillDisapper instead of dealloc.
The choice is a matter of properly pairing the addObserver/removeObserver calls.
If you call addObserver in a place that is called once such as some init method or viewDidLoad then call removeObserver in dealloc.
If you call addObserver in a place that is called multiple times like viewWillAppear then call removeObserver in viewWillDisappear.
It's the proper pairing that matters.
One critical thing about KVO is that you MUST match removeObserve and addObserver calls and you cannot add duplicate observers. This means that you must carefully think through where you add the observer and where you remove it so that you don't violate either of those restrictions.
If you add it in viewDidLoad it is currently sufficient to remove it in dealloc (since viewDidUnload isn't used any more), but the observer may trigger when the view isn't visible. If you're running on older OS'es where viewDidUnload is still called, this can also be problematic as you have to track when the observers are in place and when they aren't.
You can add it in viewDid/WillAppear, in which case you need to remove it in viewDid/WillDisappear. This is usually cleaner since the calls are (generally speaking) guaranteed to be matched up.
The link you reference is relatively informative and accurate.
I think of it in these terms:
a) does the VC need to be notified while it's on screen only? use viewWillAppear/viewWillDisappear.
b)does the VC need to be notified as long as it's alive (but not necessarily on screen)? use init or viewDidLoad and remove in dealloc.
I've breakpointed my dealloc methods under ARC and they're called when expected. However viewDidUnload is not called.
So with viewDidUnload deprecated as of iOS 6, what do I need to do now?
Delete it, and migrate all of it's contents in didReceiveMemoryWarning, or leave it, and don't do anything in didReceiveMemoryWarning?
The short answer is that, in many cases, you don't need to change anything. And, you most certainly do not want to simply migrate all of the contents of viewDidUnload to didReceiveMemoryWarning.
Generally, most of us do the setting of IBOutlet references to nil in viewDidUnload (largely because Interface Builder would put that there for us) and do the general freeing of memory (e.g. clearing of caches, releasing of easily recreated model data, etc.) in didReceiveMemoryWarning. If that's the way you do it, then you probably don't require any code changes.
According to the iOS 6 viewDidUnload documentation:
Views are no longer purged under low-memory conditions and so this method is never called.
Therefore, you do not want to move the setting of your IBOutlet references to nil anywhere, because the views are no longer purged. It would make no sense to set them to nil in didReceiveMemoryWarning or anything like that.
But, if you were responding to low memory events by releasing easily-recreated model objects, emptying caches, etc., in viewDidUnload, then that stuff should definitely move to didReceiveMemoryWarning. But then, again, most of us already had it there already.
Finally, if you free anything in didReceiveMemoryWarning, just make sure your code doesn't rely upon them being recreated in viewDidLoad again when you pop back, because that will not be called (since the view, itself, was never unloaded).
As applefreak says, it depends upon what you were doing in viewDidUnload. If you update your question with explicit examples of what you had in your viewDidUnload, we can probably provide less abstract counsel.
The short answer:
Never use -didReceiveMemoryWarning for a balanced tear down as it might get called never or multiple times. If you have your setup in -viewDidLoad, place your cleanup code in -dealloc.
The long answer:
It's not easy to give a general answer, as it really depends on the situation. However, there are two important facts to state:
1. -viewDidUnload is deprecated and in fact never called starting with iOS6 and later. So, if you have your cleanup code in there, your app leaks under these OS versions
2. -didReceiveMemoryWarning might be called multiple times or never. So it's a really bad place for a balanced teardown of objects you created somewhere else
My answer is looking at the common case where you are using properties, for example:
#property (strong) UIView *myCustomView // <-- this is what I'm talking about
#property (assign) id *myDelegate
Here you have to do some cleanup, because you either created and own the customView or InterfaceBuilder created it, but you are retaining it. Before iOS 6 you would probably have done something like this:
- (void)viewDidLoad {
self.myCustomView = [[UIView alloc] initWithFrame:…];
}
- (void)viewDidUnload { // <-- deprecated!
[myCustomView removeFromSuperView];
self.myCustomView = nil;
}
...because (again) myCustomView is a retained property, created and owned by you and so you have to take care and "release" it (set it to nil) at the end.
With iOS 6, the best place to replace -viewDidUnload and to set the retained property to nil is probably -dealloc. There's also viewWillAppear and viewDidDisappear, but these are not tied to the lifecycle of your view/controller, but the display cycle (On the other hand, the -...appear methods are perfect for un-/registering notification listeners!). So it might not be appropriate to create and destroy views before and after every display. dealloc is the only method we can be sure of to be called at the very end of the controller's lifecycle. Note that you must not call [super dealloc] if you're using ARC:
- (void)dealloc {
self.myCustomView = nil;
}
However, if you're using viewDidLoad to do some view related setup that can be freed upon low memory conditions, the other posts showing how to deal with low memory situations are totally valid. In this case you can also use dealloc, but you have to check if your views are still there.
The Lifecycle of a ViewController
Maybe it's also helpful to look a the general lifecycle of a ViewController:
This is the lifetime of a viewController (lines in italic mean that these methods might be called multiple times):
init: ViewController loaded, no interface element (IBOutlet) available yet (all nil)
viewDidLoad: the nib/storyboard has been loaded and all objects are available. The user sees nothing yet
viewWillAppear: the view is about to be displayed
viewDidAppear: the view is on screen
viewWillDisappear: the view is about to go away
viewDidDisappear: the view just was taken off the window
viewDidUnload: NEVER CALLED in iOS6/7
didReceiveMemoryWarning: You don’t know if, when and how often this is called. Prior to iOS6 it might unload the view, after iOS6 it just purges an offscreen cache or does nothing
dealloc: the viewController is about to get destroyed
So, to sum it up there are various possibilities; what goes where now really depends on what was initialized where:
-dealloc if created in -init: or -viewDidLoad:
-viewWill/DidDisappear (paired with -viewWill/DidAppear)
-didReceiveMemoryWarning (might or might not be called)
If you need to know if your UIViewController is dismissing you can add this code to your viewWillDisappear:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ([self isBeingDismissed] || [self isMovingFromParentViewController])
{
// Do your viewDidUnload stuff
}
}
It is not called at the same time as viewDidUnload did once in the view controller life cycle, but works in most cases for your needs!
Depends on what you do in viewDidUnload but you can use didReceiveMemoryWarning or dealloc to release data. See this.
In most typical cases, this method can be used in place of the old viewDidUnload.
// The completion handler, if provided, will be invoked after the dismissed controller's viewDidDisappear: callback is invoked.
- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^)(void))completion NS_AVAILABLE_IOS(5_0);
Swift 2017 syntax:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if (yourChildViewController != nil) {
print("good thing we did this!")
}
yourChildViewControllerProperty = nil
super.dismiss(animated: flag, completion: completion)
}
viewDidUnload is no longer called in iOS6, so as a workaround for an app that does some necessary things in viewDidUnload I have done this:
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// only want to do this on iOS 6
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 6.0) {
// Don't want to rehydrate the view if it's already unloaded
BOOL isLoaded = [self isViewLoaded];
// We check the window property to make sure that the view is not visible
if (isLoaded && self.view.window == nil) {
// Give a chance to implementors to get model data from their views
[self performSelectorOnMainThread:#selector(viewWillUnload)
withObject:nil
waitUntilDone:YES];
// Detach it from its parent (in cases of view controller containment)
[self.view removeFromSuperview];
self.view = nil; // Clear out the view. Goodbye!
// The view is now unloaded...now call viewDidUnload
[self performSelectorOnMainThread:#selector(viewDidUnload)
withObject:nil
waitUntilDone:YES];
}
}
}
Is there any precedent for Apple rejecting something like this? Due to time constraints I can't risk them rejecting anything.
There's no reason why Apple would reject it, but they removed it for a reason.
Calling it manually is just asking for problems. I strongly recommend to get the application logic right and do it the "correct" way.
Edit: After looking over that code again, I have serious doubts about your implementation. The whole construct with calling selectors (we should already be on the main thread!) and removing from superview (which just steals the view from it's owner without telling it) just cannot be correct. This is the type of code that you really want to eliminate from your code base.
There's no reason they would reject this. By deprecating the method, -[UIView viewDidUnload] simply becomes like any other method. You can treat it as if it never existed on UIView in the first place and you just happened to create a method called -viewDidUnload.
It would be a problem if -viewDidUnload still existed internally (which it might) and you attempted to call Apple's (now private) implementation instead of your own but I highly doubt Apple would do this. Just remember in your -viewDidUnload to remember to ask if the super class implements the method before attempting to call it on super if that's what you're currently doing, using:
if ([[self superclass] instancesRespondToSelector:#selector(viewDidUnload)]) {
[super viewDidUnload];
}
If you really wanted to be safe you could always move your code to a different method. Inside viewDidUnload just call your new method for iOS 5 devices and in -didReceiveMemoryWarning call your new method if you're on iOS 6.
I'm not going to comment in length on the logic in your didReceiveMemoryWarning since that wasn't in the question but I will say that you should be very careful about the state you're putting the controller in (make sure those if statements cover all of your bases!) And of course, you cannot expect your view controller and its views to be in the same state when viewDidUnload is called by you as when it was called by UIKit in iOS 5.