I have seen many similar questions on SO and as far as I can see all of them suggest that we should not push/pop any views while either the current view 'viewWillAppear' has not being called or the 'isViewLoaded' call returns false.
In my instance, I try to push a new view in the current view 'viewWillAppear' method and before I push it I also call [self isViewLoaded] to see if the current view is loaded (which is) yet I still keep getting the above error.
The only way I was able to solve this (which is a hack) is to sleep for few seconds before I call the push on a different non UI thread.
Since sleeping for few seconds before the call actually makes the pushing works, I assume that although both 'viewWillAppear' is already called and [self isViewLoaded] is true, it is still be sometimes too early to push/pop another view (maybe do to animation still not complete?).
I'm I missing something? Is there any other method other than 'viewWillAppear' or [self isViewLoaded] that will indicate %100 that the view has completely loaded and all animations are complete so I can safely push/pop other views?
you could try it in viewDidAppear.
however, what scenario/design you had in mind when implementing this?
IMO, it is a bad design to immediately push a new view controller from the view that isn't loaded yet.
Related
I'm following the NSOperation tutorial: http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues to load images in my own view controller's tableview. While the images are loading, if I pop the view controller from my navigation controller, the app crashes with the error: EXC_BAD_ACCESS on the line:
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO];
The line tells my delegate(view controller) to perform a method, but I already popped my view controller. How do I prevent this from happening? I tried cancel my operation queue in the viewDidUnload of my view controller, but it doesn't work. Also tried perform a check if my delegate is nil before performing the method. Any help would be greatly appreciated.
How are you popping your view controller?
For simplicity, let's name the method where you pop your view controller pop.
I would implement something in pop where before you actually pop the view controller you call cancel on your NSOperation (perhaps show a loading spinner or an overlay here). Then wait until the cancelled and finished values are true and executing is false (or at least check some of these and experiment to see which ones you can get away with not checking).
The important thing is that there is logic inside each block you add to the NSOperation that checks to see if cancel has been called. But, this is tricky as if your block just checks at the beginning if it should cancel it might be fine at that point to carry on. Perhaps it then goes on to perform some async task and if at that point you call cancel it won't do anything as your block is no longer guarding against a cancellation call.
My guess is that you're calling cancel but not actually waiting until the operation has gone away, done its KVO and cancelled everything.
I'm not sure if your question purely regarded learning about NSOperation but, if it wasn't, AFAIK, AFNetworking does a great job at all of this stuff. Particularly at loading ImageViews asynchronously (which is what that tutorial seems to be covering with that NSOperation stuff). AFNetworking adds a category to UIImageView called UIImageView+AFNetworking that provides a method called setImageWithURL:. This takes care of all these sort of problems and provides caching as well. Install AFNetworking, then
#import "UIImageView+AFNetworking.h"
and call
[self.imageView setImageWithURL:imageURL];.
I know that this last bit doesn't specifically answer your question but hopefully the rest helps.
I use the following to open a new view controller
ViewController *mainMenu = [self.storyboard instantiateViewControllerWithIdentifier:#"mainview"];
[self.navigationController pushViewController:mainMenu animated:NO];
When the mainview is active on the screen the application still runs old view controller (methods) in the background.
How i can stop the old view controller from running or make it inactive?
First, let's ask ourselves this: what kind of methods run on a view controller?
I'm going to categorize the methods that are part of a UIViewController subclass into 4 categories (though there's clearly overlap among these categories).
Life-cycle methods. These are methods such as viewDidLoad, viewWillAppear:, etc. that run as part of the view controller's life-cycle. None of these methods should be running if the view is "in the background" other than, arguably, viewDidLoad, viewWillAppear: which are technically called before the view has come to the foreground, and viewDidDisappear: which is technically called just after the view has left the foreground. So we don't really need to worry about these.
Delegate methods. Your view controller could delegate objects. The delegate methods are called on your view controller whenever the object they're delegating calls them. In the majority of the cases, we're talking about delegating some sort of UI element which is part of the view controller. This UI element would be "in the background" any time the view controller is, so it shouldn't be calling delegate methods.
NSTimer called methods. NSTimer can keep a strong reference to an object, and keep that object alive until the fire time ticks over and it calls the specified method on that object. These methods are certainly likely to be called even if the view controller is in the background, but if you want to stop the timer from running if the view controller's view isn't visible, then just invalidate it in viewDidDisappear:.
Miscellaneous methods. These methods don't necessarily into any of the above categories (though they could). Most of the time these methods are called are from within the view controller themselves, so if nothing is being called from one of the first three categories, it will be rare that this category is called (unless the method is public).
So you've got methods running "in the background" on your view controller. What category do they fall in?
If they're in the first category, then either you're calling a life-cycle method manually from somewhere (which I can't recommend against enough--there's no good reason to do this), or perhaps you don't understand the UIViewController life cycle.
If they're in the second category, then it's most likely good that they're being called. If the Object A is being delegated by View Controller A and Object A is still working on some process and still requires delegation, then it's extraordinarily important that View Controller A is still responding to the delegation methods. If you don't want this to happen, you need to stop Object A from requiring delegation.
If they're in the third category, invalidate your NSTimer objects when it's appropriate. NSTimer doesn't just magically not do what you scheduled it to do. You have to invalidate it in viewDidDisappear if you don't want it to call methods on the view controller when its view is not visible.
If they're in the fourth category, start by making sure you've not got public methods that you're calling manually from outside the view controller. If you do, well stop calling them. If you don't, go double check the first three categories.
If you mean that the view controller that was active before is still executing code, I'd suggest doing the following to remove it from view and execution before pushing:
ViewController *mainMenu = [self.storyboard instantiateViewControllerWithIdentifier:#"mainview"];
[self.navigationController popViewControllerAnimated: NO]; // Takes the old one away
[self.navigationController pushViewController:mainMenu animated:NO];
I have a MFMessageComposeViewController in my app, and sometimes the recipient count can get up to 200 or so. When testing on a 4S, it takes up to 10 seconds (!!) to load the sms view after I click my button. I imagine it could be even longer with older devices.
This is obviously too long for a blocking call without a loading view, but I can't put a loading view up if I have no callback when the view actually loads! There's only 1 callback in the MFMessageComposeViewControllerDelegate protocol, but that only calls back after the sms view is dismissed, not when it loads. Any ideas?
I'm thinking maybe grabbing callbacks from somewhere else in the modal view loading logic, or last resort I could just put a 5-10 second loading screen and hope it's up by then.
MFMessageComposeViewController is a subclass of UINavigationController. Have you tried implementing any of UINavigationControllerDelegate's methods. Specifically:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
After some more digging, I found out that I was using an older version of UIViewController's presentViewController:animated:completion: so I didn't have the "completion" part implemented. This is definitely the best option for my purposes to just dismiss my loading view in the completion block, but it does only work on iOS 5.0 and up. If you have to support lower than 5.0 maybe Edwin's answer would help.
UPDATE: Using my above method actually didn't work in practice. If I threw up a progress view and then called presentViewController:animated:completion: on the MFMessageComposeViewController, it would still make the blocking call before the progress view even showed up. I'm assuming it was something to do with the run loop not completing before the blocking call started so no UI changes would come up.
Instead I just had my progress view make a callback when it actually showed up, and then in the callback I would start loading up the MFMessageComposeViewController. Then, when the MFMessageComposeViewController callback was fired I would dismiss my progress view as well as the MFMessageComposeViewController. Success!
I'm wondering if there's any way -viewWillAppear: would be called without a matching -viewDidAppear:. Ditto for -viewWillDisappear and -viewDidDisappear.
The root of my question is where to register and unregister for KVO and / or NSNotifications of an object who's change notifications will cause the view controller to update views.
For example, I have a model object that is being processed asynchronously and it's string properties could change. I'd like the view controller to KVO those properties and have any changes reflected by swapping out the text of a label managed by said view controller.
Where do you register and unregister for notifications and why?
EDIT:
A gotcha I've come across is what to do in cases of application state change (e.g. -applicationWillResignActive, -...didEnterBackground, etc). These changes don't seem to trigger view controller lifecycle methods. Any best practices here?
With the standard container view controllers, you will always get will/did messages in pairs. If you have written your own view controller container, you may not, but that would be a bug in the container implementation.
Most of the time, you'll want to set things up and tear things down in the 'will' messages. This gives your view controller the earliest shot at anything it needs to do before it becomes "active" and also shuts down things as early as possible when you no longer need them.
When pushing a view controller in nav stack, it is entirely possible that a notification will occur during the push animation. If you set up the observers in viewDidAppear, you would miss that notification. You want to be listening as soon as possible.
Likewise, I would reckon that viewDidDisappear is too late to remove callbacks. For example, a location manager could be stopped in viewDidDisappear, but another location update could be delivered during the disappearing animation. That probably doesn't hurt much, but depending on the application, something weird could happen like an alert view appearing after you've already left a view controller - which looks like a flicker to the user.
Anything non-view related, as indicated above, occur in the 'will' methods. The choice about will vs did, then, is really about what the user sees. Animations should start in viewDidAppear, otherwise, the user won't see the frames that occur during will/did appear. Data should be moved to views in viewWillAppear, otherwise, a blank view will transition in and the data will only appear after the transition animation completes. Also, it possible that a view frames could be adjusted in between viewWillAppear/viewDidAppear, like in the case of a previous view controller in the stack hiding/showing the navigation bar.
And on a side note, not something I will get into great detail here with, but I'd advocate against KVO for controller interactions that move data from model to view objects. Difficult to test and difficult to trace.
You can subclass your UILabel and in your subclass override the setText method:
-(void)setText:(NSString *)newText {
//do your KVO updates here
[super setText:newText];
}
hope this helps
We have a container view controller and want to be able to call "PerformSelector" on one of the "sub" view controllers in that container, right after starting a transition, i.e.
[self navigateSubViewControllerTo:newSubViewController];
... some time later, elsewhere in the stack, a selector will be performed on the top visible VC
[subViewController performSelector:#selector(foo)];
The call to transitionFromViewController happens in navigateSubViewController.Unfortunately, since transitionFromViewController happens asynchronously, we are finding that the performSelector call gets applied to the "before" sub view controller, not newSubViewController. I.e. it is happening before the transition happens.
Any thoughts on how to have performSelect not happen until the sub view controller transition happens?
You can just call performSelector in the completion block of
transitionFromViewController:toViewController:duration:options:animations:completion:
UIViewController provides a callback beginAppearanceTransition:animated: which is exactly for this purpose. Simply implement it in your subviewcontrollers and your good to go ;)