I have a pretty standard setup. I have a UIViewController to handle user interaction and a somewhat long running NSThread that does the actual work when the UI Controller shows up. One issue I've had is when I want to cancel the NSThread. Standard NSThread semantics is alright. I want the thread to finish, clean itself up (so that it releases the reference to my UIViewController) and then pop the UIViewController. So my code looks something like this:
In NSThread selector:
-(void) nsThreadWork
{
// do work here
#synchronized(self)
{
[nsThreadInstance release];
nsThreadInstance = nil;
}
}
In UIViewController that spawns the thread:
-(void) startThread
{
nsThreadInstance = [[NSThread alloc] initWithTarget:self
selector:#(nsThreadWork) ...];
[nsThreadInstance start];
[nsThreadInstance release];
}
And if I want to cancel:
// assume that this will be retried until we can execute this successfully.
-(void) cancelBackgroundOpAndPopViewController
{
#synchronized(self)
{
if (nsThreadInstance == nil)
{
[self popViewController];
}
}
}
I doubt if this is correct though. The problem is, UI elements can only be manipulated from the main thread. If I pop the view controller before NSThread exits, NSThread will exit and release the view controller which means it will be dealloced from NSThread's context which will cause an assert. Everything appears to work properly with the above code, but I dont understand when the runloop deallocates NSThread. Can anyone provide any insight?
Related
It looks like this topic comes up a lot. I read through several answers but none were the same case as mine so please excuse me if you've seen similar before.
All of my UIViewControllers are being controlled by UINavgationController. On the first UIViewController (SMOnboardingPhotoMarketingViewController), I call into my keychain wrapper class to see if there is anyone logged in (app resuming). If so I call the segue to go to my main logged in screen (SMThumbnailViewController), where I'm getting the error message: Unbalanced calls to begin/end appearance transitions for .
I have examined all of the view controller life-cycle calls to ensure that I'm calling [super method] if I over-rode them. Done.
Other than that this is just a standard push type segue for all transitions. I don't understand what is so different about this call to a segue than all the others that are triggered by button actions. Here is the code from my first view controller:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self loadScrollViewContent];
__weak SMOnboardingPhotoMarketingViewController *weakSelf = self;
[SMAuthentication validateStoredTokenWithCompletion:^(BOOL valid) {
if(valid){
NSLog(#"Logged in. Continue to thumbs page");
[weakSelf performSegueWithIdentifier:kSeguePhotoMarketingToThumbnails sender:self];
}
else{
[SMAuthentication logOut];
NSLog(#"invalid credentials stored. User must log in ");
}
}];
}
I've noticed that in my main view controller (the one that the above code navigates to), viewDidLoad is called, but viewDidAppear is never called. What could cause such an imbalance?
Edit: Adding info. I should state taht if I move the segue call to the outside of that block, the transition goes as normal with no error. Example:
// I know this is ugly. It is for testing only
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self loadScrollViewContent];
__block BOOL complete = NO;
__block BOOL isValid = NO;
[SMAuthentication validateStoredTokenWithCompletion:^(BOOL valid) {
if(valid){
NSLog(#"Logged in. Continue to thumbs page");
isValid = YES;
}
else{
[SMAuthentication logOut];
NSLog(#"invalid tokens stored. User must log in ");
}
complete = YES;
}];
while (!complete) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
if(isValid){
[self performSegueWithIdentifier:kSeguePhotoMarketingToThumbnails sender:self];
}
}
You probably have implemented the following ViewController custom container methods:
- (void)endAppearanceTransition
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated
I once written and forgot them in a base class and they messed up the whole appearance forwarding to child controllers when using storyboards.
I have a UINavigationController in my application where I am pushing a view controller. On that viewcontroller, there is a preloader view that shows animation while the data is being downloaded and parsed in the background. Below the animation is a button that should redirect the user to some other view controller.
Now, I can accomplish this by popping the viewcontroller since the view I need to be directed to is the view the current view was pushed from. The problem here, however, is that the downloading, parsing, timer, etc.. everything keeps happening in the background.
The functionality I need is for the viewcontroller to completely stop working as soon as I pop the VC by clicking on the button.
Thanks in advance.
What are you using to run the background operations?
You can simplify background operations by subclassing NSOperation and using NSOperationQueue to run it. You just have to release NSOperationQueue and iOS will clean up for you automatically.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/NSOperation_class/Reference/Reference.html
https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/NSOperationQueue_class/Reference/Reference.html
Here is an example.
#interface MyBackgroundOperation : NSOperation
#end
#implementation MyBackgroundOperation
- (id)init
{
self = [super init];
if(self != nil)
{
// stuff that needs to be done at init
}
return self;
}
- (void)main
{
#try
{
// run my background operations
}
#catch (NSException *e)
{
// catch the exception
}
}
- (void)dealloc
{
// clean up
[super dealloc];
}
#end
Then you start the operation like this
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
MyBackgroundOperation *myOperation = [[MyBackgroundOperation alloc] init];
[operationQueue addOperation:myOperation];
[myOperation release];
[operationQueue release];
The OS will retain the operation until it's finished so that releasing the view controller will not effect it. However, if you want to stop the operation after it's already started you're going to have to use a bit more fancy techniques, maybe notifications or KVO.
I am trying to implement search on a background thread using NSOperation on iOS. I didn't want to subclass NSOperation so this is what I'm doing:
[searchQueue cancelAllOperations];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
elector:#selector(filterContentForSearchText:)
object:self.searchDisplayController.searchBar.text];
[searchQueue addOperation:op];
[op release];
The search method includes a for loop that checks whether what is being searched is in an array. Now when I cancel the NSOperation by calling cancelAllOperations, the for loop continues to run through the array. I would like to prevent this and was wondering whether it is legit to call this from within the for loop:
if ([[[searchQueue operations] objectAtIndex:0] isCancelled]) {
[tmp_array release]; // tmp_array is used to hold temporary results
[pool drain]; // pool is my autorelease pool
return;
}
One of the reasons to subclass NSOperation is to implement proper cancellation. You could do your approach, but it violates several good design principles. Basically, since cancellation requires the cooperation of the operation itself, NSInvocationOperation isn't built to cancel the invocation while it's already executing (though it can be successfully cancelled before it starts executing), as the running method shouldn't know anything about how it's called.
Instead, if you subclass NSOperation, you can put most of this functionality into the main method very easily:
#implementation MyOperation
- (void)main {
if ([self isCancelled])
return;
for (...) {
// do stuff
if ([self isCancelled]) {
[tmp_array release];
return;
}
}
}
#end
Note also that you don't have to maintain your own autorelease pool with such an implementation.
I have a tableView that loads an XML feed as follows:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([stories count] == 0) {
NSString *path = #"http://myurl.com/file.xml";
[self parseXMLFileAtURL:path];
}
}
I'd like to have the spinner to show on the top bar at the app launch and dissapear once the data is displayed on my tableView.
I thought that putting the beginning at viewDidAppear and the end at -(void)parserDidEndDocument:(NSXMLParser *)parser but it didn't work.
I'd appreciate a good explained solution on how to implement this solution.
Here's the problem: NSXMLParser is a synchronous API. That means that as soon as you call parse on your NSXMLParser, that thread is going to be totally stuck parsing xml, which means no UI updates.
Here's how I usually solve this:
- (void) startThingsUp {
//put the spinner onto the screen
//start the spinner animating
NSString *path = #"http://myurl.com/file.xml";
[self performSelectorInBackground:#selector(parseXMLFileAtURL:) withObject:path];
}
- (void) parseXMLFileAtURL:(NSString *)path {
//do stuff
[xmlParser parse];
[self performSelectorOnMainThread:#selector(doneParsing) withObject:nil waitUntilDone:NO];
}
- (void) doneParsing {
//stop the spinner
//remove it from the screen
}
I've used this method many times, and it works beautifully.
Starting a new thread can be overkilling and a source of complexity if you want to do things that are supposed to start on the main thread.
In my own code, I need to start a MailComposer by pushing a button but it can take some time to appear and I want to make sure the UIActivityIndicator is spinning meanwhile.
This is what I do :
-(void)submit_Clicked:(id)event
{
[self.spinner startAnimating];
[self performSelector:#selector(displayComposerSheet) withObject:nil afterDelay:0];
}
It will queue displayComposerSheet instead of executing it straight away. Enough for the spinner to start animating !
I typically implement an NSTimer that will call my spinner method, which I fire off right before I go into doing the heavy work (the work that will typically block the main thread).
The NSTimer fires and my spinner method is called. When the main work is finished, I disable the spinner.
Code for that is like:
IBOutlet UIActiviyIndicatorView *loginIndicator;
{
...
[loginIndicator startAnimating];
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(executeAuthenticationRequest)
userInfo:nil repeats:NO];
...
}
- (void) executeAuthenticationRequest
{
/* Simulate doing a network call. */
sleep(3);
[loginIndicator stopAnimating];
...
}
You can also do:
IBOutlet NSProgressIndicator *pIndicator;
Start:
[pIndicator startAnimation:self];
[pIndicator setHidden:NO];
And Stop:
[pIndicator stopAnimation:self];
[pIndicator setHidden:YES];
In Cocoa (and most other app frameworks) the user interface is updated by the main thread. When you manipulate views, they are typically not redrawn until control returns to the run loop and the screen is updated.
Because you are parsing the XML in the main thread, you are not allowing the screen to update, and that is why your activity indicator is not appearing.
You should be able to fix it by doing the following:
In viewDidAppear, show/animate the spinner and then call
[self performSelector:#selector(myXMLParsingMethod) withObject:nil afterDelay:0];
In myXMLParsingMethod, parse your XML, then hide/stop the spinner.
This way, control will return to the run loop before parsing begins, to allow the spinner to begin animating.
Consider this:
#interface SomeViewController : UIViewController {
SomeChildObject *child;
}
#end
#implementation SomeViewController
- (void) viewDidLoad {
...
child.delegate = self;
}
- (void) somethingHappened {
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:child
selector:#selector(doSomething)
object:nil];
[someNsOperationQueue addOperation:operation];
[operation release];
}
- (void) callbackA:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackAonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackAonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
- (void) callbackB:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackBonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackBonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
#end
In English:
I have a view controller running in the main thread with a child model object to do something (fetch data over network). The view controller is the delegate for the child, so the child can signal the results back with delegation. To perform the expensive work, I spawn the child.doSomething method with a NSInvocationOperation that launches the operation in a background thread. When done, the child calls the delegate's (view controller's) callbackA or callbackB with some results. Since (I think) these callbacks are invoked in the same background thread where the doSomething call was run, I need to call performSelectorOnMainThread to transfer control back to main thread.
This works fine, but I do not like having two callback-related methods for each callback. (There are actually more callbacks, so the real code is even more bloated.) Ideally, I would do something like this:
- (void) callbackA:(SomeData *)someData {
if (not_running_on_main_thread) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
} else {
// now running on main thread, work with the results.
}
}
Questions:
1) how do I do the "not_running_on_main_thread" test?
2) is there any other way to cut down the callbacks bloat?
EDIT: ok, I never read NSThread docs before posting :) looks like [NSThread isMainThread] is what I am looking for. But is there any other way to restructure or make this nicer still?
Just check for [NSThread isMainThread]. There's nothing more you can do if you need multiple callbacks which do different things.
Only one thing I do differently, my code looks like this:
- (void) callbackA:(SomeData *)someData {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
return;
}
// now running on main thread, work with the results.
}
This lets me get rid of the whole-function-long else and make the code a little clearer. And you can save on one indentation level this way ;-)