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.
Related
Let's look at a typical RESTful iOS app, let's say a contact app, main screen is the list of contacts, when tapping on a contact you get to the contact detail screen.
The contact list is obtained through a REST API, and the contact details through another API.
Which event would you use to trigger the call to those APIs:
viewDidAppear on both view controllers
viewWillAppear on both view controllers
From the main view controller, call the contact detail API before calling the pushViewController:detailViewController
Any other events?
Currently I am using viewWillAppear mostly for this kind of scenario, or viewDidAppear in some specific cases, but in an effort to standardize my coding practices, I would like to definitely settle on the pros/cons of those various approaches.
It's partly a matter of preference. Since the API call will generate an unknown delay, the app should present UI that indicates it's busy. My preference is to have the UI do as much as possible before the request. (My naive model of cognition is that looking at the new VC's UI while it's fetching data will occupy user's mind for an instant, making the lag seem that much shorter).
So I favor paramaters to the VCs that describe the request - like the id of the contact to be fetched on the detail VC, and do the request on viewDidAppear (if the data isn't already cached or needs a refresh). In that method, put up some UI to indicate the fetch is happening, so it has the form:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (/* i don't have my model or it's out of date */) {
// put up 'i am busy' UI
MyRequestClass *request = // form a request that fetches my model
[request runWithBlock:^(id result, NSError *error) {
// i build my request classes to run with blocks simplifying the caller side
// if it's a json request, then pass a parsed result back to this block
// remove 'i am busy' UI
if (!error) {
// init my model from result
// other parts of this class observe that the model changes and updates the UI
} else {
// present error UI
}
}];
}
}
Firstly, it's good practice to ensure your API interface and data access is happening outside of your view controllers in a separate data access class (or data controller - if you're already doing this then apologies, and ignore this paragraph). You want to avoid putting networking code directly into your view controller, because it's going to make your life very difficult if you either want to create an iPad specific view or need to revamp your UI in some way later on down the line).
With that out of the way, you have several options. In terms of performance from the user's perspective, it's best to pre-fetch as much from your RESTful API as you can. This is what libraries such as AFIncrementalStore that map your API to Core Data try to do. But if you have many thousands of contacts, are heavily rate limited, or bandwidth constrained this is going to be problematic.
What's absolutely certain is that you want to make the call to your networking API as soon as possible so that the user experiences minimal delay. You may find using viewDidLoad rather than viewWillAppear or viewDidAppear may work better in this case: you can set your view up with a loading/holding graphic or animation, trigger your asynchronous networking call, and then once complete display the required information.
I usually do this:
Load the contacts on viewWillAppear, and if you have pull-to-refresh, when that happens.
When user taps on a cell, in the method that handles that event, load event details, and pass that object to the contact details controller's constructor, and push it.
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.
I wrote some sample code in a separate application, that basically queues up items in an NSMutableArray. I then have a method which loops through that array and creates an NSInvocationOperation, puts it in the NSOperationQueue, releases the operation and so forth. The method then gets called (simply prints out the string that was stored in the array and passed into the operation), does it's job, and sends an NSNotification back to the ViewController. The observer method gets hit, but the operations have a lag on them. For instance, the observer method simply updates a UILabel with how many messages are left in the queue. It eventually does this, but there seems to be a five second lag in between all of the NSOperations completing and the UI updating. To me it seems like the NSOperationQueue is blocking the main thread. Is there anyway to get the UI to respond immediately to the notifications?
One important note is that I have not tested this on the phone yet, just the simulator. I'm not sure if this makes a difference.
Sorry in advance. I'm away from my computer and I don't have the code in front of me. Hopefully I explained it well enough. Also I have read the documentation, just haven't found anything that's really answering this specific question for me.
The delay is typical of UI updates that are performed on threads other than main.
To see your update instantly on the UILabel, be sure to invoke whatever method is updating the label's text as follows:
[self performSelectorOnMainThread:#(myMethodToUpdateLabelWithText:) withObject:text waitUntilDone:NO];
where myMethodToUpdateLabelWithText is a method within your class that sets the label's text value.
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.
Are there any race condition issues when using NSNotifications within a single thread? Here is a sample method:
- (void) playerToggled: (NSNotification *) notification {
if (timerPane.playing && ! timerPane.paused) {
[playerPane toggleCurrentPlayer];
[timerPane toggleTimer];
[mainPane playerToggled];
}
}
The first two calls after the condition will trigger NSNotifications that will be received by mainPane. Is mainPane guaranteed to receive the playerToggled message after those notifications? I should say that this code seems to work as desired (playerToggled always executes last). But I'm not sure what timing issues there are around notifications and I can't find a specific answer.
There are no race conditions to be expected. In addition to Dan Donaldson's answer, here is another quote from the docs for 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.
I am not exactly sure what you mean, but I think this will be helpful to you:
http://developer.apple.com/iphone/library/documentation/Cocoa/Conceptual/Notifications/Articles/NotificationQueues.html#//apple_ref/doc/uid/20000217
Especially this part:
Using the NSNotificationCenter’s postNotification: method and its variants, you can post a notification to a notification center. However, the invocation of the method is synchronous: before the posting object can resume its thread of execution, it must wait until the notification center dispatches the notification to all observers and returns.