In almost every UIViewController I have a bunch of AFHTTPRequestOperations and I have to properly handle any kind of cancel (pressing cancel button, going back in UINavigationController's stack, etc.). I was wondering if creating one NSOperationQueue per each UIViewController and adding to it all operations called within controller would be a proper way to go? I was aiming for cancelling all operations [[NSOperationQueue mainQueue] cancelAllOperations] but this will kill all operations already started, especially those called from previous UIViewController. Or should I create property for each operation, call it in viewWillDissappear:(BOOL)animated and set if statement for cancel state in success block?
AFHTTPRequestOperationManager instances are cheap to create and each has its own operation queue, so it is easy to cancel all of a given UIViewController's operations:
- (void)dealloc {
[self.requestOperationManager.operationQueue cancelAllOperations];
}
This will cancel any request created through self.requestOperationManager. You can create the AFHTTPRequestOperationManager in your UIViewController's init method.
I recommend cancelling operations in your view controller's dealloc method, as you know it will no longer be needed.
Tried David Caunt answer, but didn't work for me. dealloc wasn't called when I pushed back button, so I guess the best way to cancel operations is to call it in viewWillDisappear: (my bad was to use && not || - silly mistake, but easiest things are often hardest to find)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController && self.isBeingDismissed){
DDLogDebug(#"back or dismissed!");
[self.manager.operationQueue cancelAllOperations]; }
}
Related
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.
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.
In my case, I presented a containerViewController consists of several UIViewControllers.
One of them, controller A, will send request to server every 10 seconds to get data. I used a RACSignal to do it:
[[[RACSignal interval:10 onScheduler:[RACScheduler mainThreadScheduler]] takeUntil:self.rac_willDeallocSignal] subscribeNext:DoSomeThing];
But when the containerViewController is dismissed from the rootViewController, the signal still fired every 10 seconds, means rac_willDeallocSignal of controller A is not called. How can it be fixed?????
Thanks!!!
Maybe it is too late to answer this question, but yet it may be helpful for those who are looking for solution.
I have solved similar problem with creating separate signal when UIViewController will disappear, and used that signal as takeUntil: in the Interval signal. Code looks like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
RACSignal *rac_viewWillDisappear =
[self rac_signalForSelector:#selector(viewWillDisappear:)];
[[[RACSignal interval:10 onScheduler:[RACScheduler mainThreadScheduler]]
takeUntil:rac_viewWillDisappear] subscribeNext:^(id x) {
//Do what you need
}];
}
The interval signal is infinite, it will never complete. Consequently, any objects that are strongly captured in the subscription blocks will also live on indefinitely, and thus their willDeallocSignal will not do anything. There are two ways to work around this:
Capture objects weakly
Explicitly dispose of the subscription
The first case is preferable. In this case, you could use #weakify(self) outside of the block and #strongify(self) inside the block.
The second option is more of a clumsy brute approach. I wouldn't recommend it.
See RAC's Memory Management.
I'm trying to implement the iOS Background Fetch API in an app. The app downloads JSON from a server, calls -[UICollectionView reloadData], and for every cell, and image is downloaded asynchronously in -collectionView:cellForItemAtIndexPath:.
In my initial implementation, I would call the completion handler passed by the system into application:performFetchWithCompletionHandler after I called reloadData. The app snapshot in the multitasking view would then display empty cells, because the images wouldn't have been downloaded yet. To solve that, I removed the completion handler call after reloadData, wrote a little structure keeping track of which cells' images have been downloaded, and only after a certain number have been downloaded, I would call the completion handler.
I did this using a completionBlock property on the view controller that reloads the images. The app delegate sets that property, then calls a reload method on that view controller, which then calls its completion handler property. I looks like this:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
WSViewController *viewController = (WSViewController *)navController.topViewController;
viewController.completionBlock = ^(BOOL success, BOOL newData) {
if (!success) {
completionHandler(UIBackgroundFetchResultFailed);
} else if (success) {
if (newData) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
};
[viewController reload];
}
During testing, I found that performing a background fetch in relatively rapid succession would cause the completion handler not to be called. That's easily explained, because the completionBlock property is overwritten and the old one won't get called.
So, as advised in WWDC 2013 Session 204 "What's New With Multitasking", I removed the property and decided to pass the completion handler all the way through my code. -reload is now -reloadWithCompletionBlock: etc.
But now I'm stuck on how to implement that in the App Delegate. The View Controller has a delegate, and one of its methods, - (void)didFinishDownloadingImages, is implemented by the App Delegate. That's the point where I want to call the completion handler. But I can't, since there is no way to get to the completion handler without storing it in a property, defeating why I was doing it this way in the first place.
Any thoughts on how to solve this?
use NSOperationQueue and move your blocks of code into a subclass of NSOperation -- this will help you handle situations like blocking, discarding multiple requests and otherwise handling all those types of situations.
So, rather than write inline blocks, I'd move to an operation queue which seems like some lifting, but actually makes this much easier to handle properly.
Say I have a subclass of UIView which I will call AnimatableView, where my implementation of layoutSubviews lays out some internal content.
AnimatableView also has the method commenceAnimationLoop which looks something like this:
- (void) commenceAnimationLoop {
NSOperation *animationLoop = [[AnimationLoopOperation alloc]init];
[queue addOperation:animationLoop]; //queue is a NSOperationQueue iVar of AnimatableView
[animationLoop release];
}
The AnimationLoopOperation is a subclass of NSOperation whose main method will start an infinite animation which only exits once the isCancelled property becomes YES.
The problem I am having is in my ViewController where I setup and animate an instance of AnimatableView like so:
- (void) viewDidLoad {
AnimatableView *animatableView = [AnimatableView alloc]initWithFrame:someFrame];
[self.view addSubview: animatableView];
[animatableView commenceAnimationLoop];
[animatableView release];
}
The problem is that the NSOperation created in the commenceAnimationLoop executes before my instance of AnimatableView has had chance to call layoutSubviews (meaning that it's content has not yet been generated and laid out).
I am making an educated guess that the layoutSubviews method is called later (when the ViewController is ready to render its view hierarchy?), but that the commenceAnimationLoop method spawns a thread that immediately executes in the background.
To summarise - what would be the best way to make my animationLoop operation wait for the view to layout before commencing?
To provide some high level context for what I am trying to achieve - I am using NSOperation to start a continuous animation. The idea being that I can provide an endAnimation method which would likely call cancelAllOperations on my queue. All this is done in the hopes I can provide a way of starting and then interrupting an infinite animation with the two methods commenceAnimationLoop and endAnimation. Perhaps I am barking up the wrong tree going down the route of multi-threading?
If you need layoutSubviews to run before your operation then you should't start the operation queue before it that. You can prevent an operation queue from starting additional operations using [myOperationQueue setSuspended:YES];.
Since it will only prevent the queue from starting new operations you should suspend the queue before you add your operations to it.
After you view has loaded and layoutSubviews has run you can resume the operation queue again using [myOperationQueue setSuspended:NO];. One place to do this could be in viewDidAppear.