I have a UITableViewController that I'd like to notify once the data of the corresponding model is ready to be displayed. The problem is that this data is fetched from a web service and the request can take up to several seconds. Currently, I'm fetching the data synchronously on the main thread which, of course, causes my main thread to block. Now, I don't want my controller to know anything about downloading data from the internet. How can I accomplish this. Currently, I'm thinking about utilizing GCD and implementing a method like -loadDataWithCallback: and provide a callback that triggers a [tableView reloadData] on success. Is this a good approach? Are there other possibilities to notify a controller that the model is ready? An other idea I had was to use a delegate mechanism and setting the controller as a delegate of my model?
To summarize, what's better: GCD with callbacks or implementing your own delegate mechanism?
Are there other possibilities?
Update: June, 24th 2011 13:15 CET
After reading all your replies, I come to the conclusion that there are 3 possible solutions to my problem:
Make use of NSNotifications and use NSURLConnection to implement async. download
Implement a custom protocol and use a delegation mechanism. Again, use NSURLConnection to implement async. download.
Use synchronous download in a separate GCD queue and use callbacks.
Since nobody favors the last solution, I want to discuss this approach a little in depth. After seeing all the code that is involved in notification handling, I think that GCD is a better approach. Instead of agreeing on a certain notification which has to be somehow documented, so that every developer knows about it, I can simply use a callback. On the one hand, it gives me a clear interface like one I would have when I would use a delegate, on the other hand, it give me total flexibility. Do you really think that GCD is to complicated for this? Here is my code:
- (void)loadRestaurantsWithCallback:(void (^)())callback
{
dispatch_queue_t current_queue = dispatch_get_current_queue();
dispatch_queue_t download_queue = dispatch_queue_create("Download queue", NULL);
dispatch_async(download_queue, ^{
self.restaurants = [self loadRestaurants];
dispatch_async(current_queue, ^{ callback(); });
});
dispatch_release(download_queue);
}
Btw., my application simply displays the menus of the different canteens at my university.
In my controller, I simply do the following:
if (![self.canteen hasRestaurants]) {
[self.canteen loadRestaurantsWithCallback:^{
[self.tableView reloadData];
}];
}
It works like a charm. What do you think about this solution?
Update: June, 24th 2011 16:30 CET
There is a fourth solution to this problem and it's probably the way to go even if it involves more code than the GCD approach. Here is what I came up with:
Use NSURLConnection to do asynchronous downloading.
Have your model respond to the callbacks sent by the NSURLConnection instance.
Use Key-Value Coding and Key-Value Observing.
In your controller, you simply have to do the following:
[self.model addObserver:self forKeyPath:#"method-name" options:0 context:NULL];
and
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[self.tableView reloadData];
}
The best way to use the NSNotificationCenter and create a local notification type and post it once you get the data.
First register for the notification type DataUpdateNotification .
- (void)viewWillAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(receiveDataNotification:)
name:#"DataUpdateNotification"
object:nil];
...............
}
Implement receiveDataNotification: to handle DataUpdateNotification type notification.
- (void) receiveDataNotification:(NSNotification *) notification
{
if ([[notification name] isEqualToString:#"DataUpdateNotification"])
{
NSLog (#"Successfully received the Data Update notification!");
}
}
Remove the notification from your object instance when your controller is disappeared.
- (void)viewWillDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Now Post the notification from any part of your application ..
- (void) DataUpdated
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"DataUpdateNotification" object:self];
}
My suggestion is using asynchronous communication instead of multi threading (GCD). It is much easier to deal with and it does what you need.
The idea is:
you execute the web request async, so you don't block;
when issuing the request asynchronously, you register a "callback" with it;
when the data is there, the underlying communication infrastructure calls the "callback" so that you know data is there;
the callback is typically a delegate of some of your classes (even you controller will do), and its main duty is updating your model, and issuing a reload of the table view.
It is a very simple and clean paradigm to use.
So, you can investigate the async possibilities offered by NSURLConnection/NSURLRequest, or (strongly encouraged) have a look at ASIHTTRequest, which will make your like much easier.
How about implementing a delegate for your processing class and using the delegate method to inform the caller.
Its pretty simple and easy to get hold off and works perfectly in such async scenarios.
Define delegate protocol and create a object for the class. Implement the delegate methods in the calling object class and when action is complete call the delegate.
I'd be happy to write some code here if you need.
if you are using threads.. you can use
[self performSelectorOnMainThread: #selector ( // ) waitUntilDone:YES]
This will make sure that your process waits until another process gets Done.
I have a similar situation and I use NSFetchedResultsController to solve it. The table is updated with the fetched results controller, and a client runs asynchronously. When data is received I create a model and write it using CoreData, at which time it is automatically detected by the fetched results controller for an insert, update, or delete into the table.
Related
I have an NSAutoreleasePool thread that is designed to pull information down from a web service, i have the web service code working nicely and i can trigger the thread to start in a view controller without any trouble, in fact its working quite nicely.
I want to:
move the thread instantiation to the appDelegate - easy!
have it run periodically and somehow tell the viewcontrollers under it (5 - 10) if new information is downloaded
have the capacity to manually execute the thread outside of the scheduler
I can fire up a method on the appdelegate using performSelectorOnMainThread but how i can get my child view controllers to "subscribe" to a method on the appdelegate?
Using NSNotificationCenter you can post well, notifications :D
That way without the appDelegate nowing the other classes the other classes can "subscribe" to the notifications they need.
Also, i would keep the thread alive, spawning a new thread everytime is costly, ofc only if it is spawned often. I would recommend using GCD ( iOS 4+ )
Here's what you do:
From the class sending the message, post a notification like :
[[NSNotificationCenter defaultCenter] postNotificationName: #"YOUR_NOTIFICATION_NAME" object: anyobjectyouwanttosendalong(can be nil)];
In the view controllers where you want to be notified of the notification when posted:
In the viewDidLoad do:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(METHOD_YOU_WANT_TO_INVOKE_ON_NOTIFICATION_RECEIVED) name:#"YOUR_NOTIFICATION_NAME" object:sameasbefore/nil];
Important! Don't forget this in your viewDidUnload():
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"YOUR_NOTIFICATION_NAME" object:sameasbefore/nil];
I'm not very sure about the object associated with notifications but you can look that up here
NOTE: When it's only one object notifying another one, you're better off using protocols :) But in this case since there are multiple view controllers listening, use notifications
Use NSNotificationCenter to send events that your view controllers are observing?
I have a view that includes a UIWebView as well as an iAD AdBannerView.
To optimize the experience and reduce bandwidth contention - I suspend the UIWebView's network activity when the iAd "detail view" is being loaded and resume it when the user returns from the ad. Currently, I simply do the following:
-(BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave
{
if (self.webView.loading) {
[self.webView stopLoading];
self.loadingWasInterrupted = TRUE;
}
return YES;
}
-(void)bannerViewActionDidFinish:(ADBannerView *)banner
{
if (self.loadingWasInterrupted) {
//Should use loadRequest: instead?
[self.webView reload];
}
}
I'm trying to understand if it there is any difference between calling reload vs. loadRequest: a second time, and if so, which is more efficient.
I'm guessing reload simply just saves you having to hold onto the request object and really does the same thing but I'd like to know for sure. The docs and header don't offer any clue.
I understand I could pick apart the network activity to understand what's happening but would appreciate any insight from someone who has looked at this before or who generally understands if reload behavior differs from loadRequest at all. Thank you.
Okay, a fairly complicated solution but never the less one that I think might work for you:
Define a custom protocol handler for http:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html#//apple_ref/doc/uid/10000165i
Using NSURLProtocol as the subclass. The handler will start a NSURLConnection, and return the data as it comes in to the client (in this case this will be the UIWebView that initiated the connection). Have it add itself as an observer to NSNotificationCenter for a "Pause" notification.
When you would like to display an iAd, you can send your pause notification, this will cancel the NSURLConnection (but not the NSURLProtocol, which will remain open and loading, and thus your webview will continue to appear as if it were loading).
Then when the add is finished you can send a "resume" notification (much the same), except in this case any active NSURLProtocol handlers receiving the notification will create new NSURLConnections, using the Range: header to resume where they left off:
iphone sdk: pausing NSURLConnection?
Some caveats:
Only will work when browsing websites that support the resume header (otherwise you might have to start the connection anew, and just ignore data prior to the latest byte received).
Your NSURLRequests should be formed so that they don't have a timeout. (if you want a timeout then it should be in the NSURLProtocol handlers NSURLConnections).
I'm guessing here, but I believe the reload is doing a loadRequest: internally. If you are really intent on testing this you cold add a temporary subclass to UIWebView and override the reload and loadRequest methods just for the sake of logging.
- (void)reload
{
NSLog(#"reload called");
[super reload];
}
- (void)loadRequest:(NSURLRequest *)request
{
NSLog(#"loadRequest called");
[super loadRequest:request];
}
When you call the method "loadRequest", you call it like
[webview loadRquest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.ururl.com"]]];
But when u call "reload", u just instruct to reload whatever the current request is.
So basically in latter case , u are saving urself from creating a url from string and then a request from url and that makes it pretty convenient for use.
As per case of functionality, reload itself calls loadRequest so basically there is no difference in terms of efficiency and even in speed.
However basically for ur case and in many of my cases , the thing which we want but Apple has not given us is something like:-
[webview pauseLoading];
[webview resumeLoading];
So to sum up the whole thing , use any of them but if u r lazy like me to again specify the urlrequest just use "reload"
Apologies
Sorry guys I used to think that reload must be calling loadRequest but when I tried NWCoder's method, it doesnot call loadRequest on calling reload. But I still think reload and loadRquest follows the same method for loading the page
My app starts by presenting a tableview whose datasource is a Core Data SQLite store. When the app starts, a secondary thread with its own store controller and context is created to obtain updates from the web for data in the store. However, any resulting changes to the store are not notified to the fetchedresults controller (I presume because it has its own coordinator) and consequently the table is not updated with store changes. What would be the most efficient way to refresh the context on the main thread? I am considering tracking the objectIDs of any objects changed on the secondary thread, sending those to the main thread when the secondary thread completes and invoking "[context refreshObject:....] Any help would be greatly appreciated.
In your NSFetchedResultsController handling the table, register in viewDidLoad or loadView for a notification:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidSave:) name:#"ContextDidSave" object:nil];
When the secondary thread is ready with the new data, simply save the context as usual, and then post the notification:
[[NSNotificationCenter defaultCenter] postNotificationName:#"ContextDidSave" object:managedObjectContext];
The notification will be handled in your NSFetchedResultsController using the following method:
EDIT: modified the method below taking correctly into account multi-threading, after an insightful discussion with bbum.
- (void)contextDidSave:(NSNotification *)notification
{
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[[[[UIApplication sharedApplication] delegate] managedObjectContext] performSelectorOnMainThread:selector withObject:notification waitUntilDone:YES];
}
For UI update, it can be done automatically using the NSFetchedResultsController delegate methods.
Finally, remember to add in the dealloc method of the NSFetchedResultsController the following:
[[NSNotificationCenter defaultCenter] removeObserver:self];
Unforgiven's answer doesn't handle threading correctly. In particular, the documentation states (emphasis mine):
If your application has a graphical
user interface, it is recommended that
you receive user-related events and
initiate interface updates from your
application’s main thread. This
approach helps avoid synchronization
issues associated with handling user
events and drawing window content.
Some frameworks, such as Cocoa,
generally require this behavior, but
even for those that do not, keeping
this behavior on the main thread has
the advantage of simplifying the logic
for managing your user interface.
A notification observer will be fired on whatever thread the notification was posted upon in the first place. Thus, you can't call NSTableView's reloadData directly from the notification posted by a background thread.
There is no need to use notifications at all. In your background thread, when ready to update the user interface, use any of a number of mechanisms to reload the data in the main thread -- in the thread that manages the main event loop & user interface.
[tableView performSelectorOnMainThread: #selector(reloadData)
withObject: nil waitUntilDone: YES];
You can also use Grand Central Dispatch or NSOperation to do something similar.
I've launched a delayed thread using performSelector but the user still has the ability to hit the back button on the current view causing dealloc to be called. When this happens my thread still seems to be called which causes my app to crash because the properties that thread is trying to write to have been released. To solve this I am trying to call cancelPreviousPerformRequestsWithTarget to cancel the previous request but it doesn't seem to be working. Below are some code snippets.
- (void) viewDidLoad {
[self performSelector:#selector(myStopUpdatingLocation) withObject:nil afterDelay:6];
}
- (void)viewWillDisappear:(BOOL)animated {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(myStopUpdatingLocation) object:nil];
}
Am I doing something incorrect here? The method myStopUpdatingLocation is defined in the same class that I'm calling the perform requests.
A little more background. The function that I'm trying to implement is to find a users location, search google for some locations around that location and display several annotations on the map. On viewDidLoad I start updating the location with CLLocationManager. I've build in a timeout after 6 seconds if I don't get my desired accuracy within the timeout and I'm using a performSelector to do this. What can happen is the user clicks the back button in the view and this thread will still execute even though all my properties have been released causing a crash.
Thanks in advance!
James
I found my issue, it didn't have anything to do with my calls to performSelector. I found that you have to set your MKMapView and CLlocationManager's delgate to nil before releasing them. Otherwise they will continue working even though you've released the instances and they have the potential to crash your app.
Thanks for your help Noah!.
I ran into a similar issue where I was unaware that I was scheduling multiple performSelector calls on different objects so the "self" was different in each case.
I'd recommend throwing in a NSLog(#"Self: %#",self); before each of your bits of code such as:
- (void) viewDidLoad {
NSLog(#"Self: %#",self); before
[self performSelector:#selector(myStopUpdatingLocation) withObject:nil afterDelay:6];
}
- (void) viewWillDisappear:(BOOL)animated {
NSLog(#"Self: %#",self); before
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(myStopUpdatingLocation) object:nil];
}
That will allow you to compare the SELF instances to ensure you are performing and releasing the selectors on the same object.
Are events posted by NSNotificationCenter postNotificationName processed before UI updating events?
I need to know because otherwise my current program will crash in some rare cases.
Model code:
- (void)searchFinishedWithResults:(Results *)results {
self.results = results;
// If some table cells are loaded NOW, before notication is processed, we might crash!
[[NSNotificationCenter defaultCenter]
postNotificationName:SearchResultArrived object:nil];
}
When processing the notication, I will run UITableView reloadData.
However, consider if before processing the notication, UI has to be updated. In this case -tableView:cellForRowAtIndexPath:indexPath will be called, but results object has changed, it will fetch old data.
The notifications are dispatched exactly when you call postNotification: or postNotificationName:object:, in a synchronous fashion, one observer after the other (in no particular order). In the case you show, they would be sent exactly after you assign the variable "results" and before the method ends.
Directly from Apple's documentation on NSNotificationCenter:
A notification center delivers
notifications to observers
synchronously. In other words, the
postNotification: methods do not
return until all observers have
received and processed the
notification.
To send notifications asynchronously
use NSNotificationQueue.
As an aside, I think you need to rethink your design. It sounds like you don't have sufficient separation between the view and the model.
Your data model should know what is and is not old data and should only return current data to the tableViewController. The data model should have complete control over the integrity of the data and it shouldn't be possible to force it to return the wrong data. It definitely should be impossible that the app will crash owing to such forcing.