How to request JSON data in the viewDidAppear method or viewDidLoad? - iphone

If I call the [self requestData]; from viewDidLoad my table populates itself with data just fine. If I move [self requestData]; to the viewDidAppear method the table remains empty.
Also, I'm not entirely sure if [self.mainTableView reloadData]; is working. I'm trying to move the data request and handling to the viewDidAppear method because I saw that pattern in a code example and thought it might speed up my app launch somewhat. At the moment there's quite a lag from the app launch Default.png to the rootViewController.
thanks for any help with this.
-(void)viewDidAppear:(BOOL)animated
{
[self requestData];
}
-(void)requestData {
[HUD showUIBlockingIndicatorWithText:#"Fetching JSON"];
NSError *requestError = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL
URLWithString:kURL]];
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&requestError];
NSError *jsonParsingError = nil;
if (requestError)
{
NSLog(#"sync. request failed with error: %#", requestError);
}
else
{
// handle data
publicData = [NSJSONSerialization JSONObjectWithData:response
options:0
error:&jsonParsingError];
publicDataArray = [publicData objectForKey:#"data"];
}
/*
for(publicDataDict in publicDataArray) {
NSLog(#"data output is %#",[publicDataDict objectForKey:#"title"]);
}
*/
[self.mainTableView reloadData];
NSLog(#"reload table cat id %#", categoryString);
[HUD hideUIBlockingIndicator];
}

You're trying to optimise the wrong thing.
The reason you have a lag is that your url request is synchronous, so it is blocking the main thread while waiting for the data to be received. Using an asynchronous URL request will give you far better performance benefits that moving the loading call as you are trying to do.

viewDidLoad is the right place to make your JSON request because It is called once, and will keep data into memory.
ViewDidAppear is called each time view appear, it is not really made to retain object
The "lag" is the fact that your UI is not being responsive because the JSON post request is on the main thread.

to speed up launching try to load data asynchronously, like this:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self requestData];
}];
you shoud call it inside viewDidLoad

viewDidLoad is called after the views have been created, but before they're drawn.
viewDidAppear is called after the views have been drawn. It's too late to set data here that affects the display. You could possibly load the data in viewWillAppear, but be aware that it may be called multiple times (e.g. switching between apps).
That's why setting data that affects the display works in viewDidLoad, but not viewDidAppear. viewDidLoad is called only once in a view controller's life cycle.
However, consider the user experience if your user's Internet connection is slow or the server is slow to respond. Your application will seem to "freeze" for awhile.
Instead of a synchronous request, consider using an asynchronous request. You could display a "loading" message, spinner, or something until the request returns, then update the UI directly.
Apple strongly recommends always making asynchronous requests when run from the main UI thread.

Related

CloudKit CoreAnimation error

We are beginning to work with CloudKit and have been working with Firebase for a while.
The issue we are having is a CoreAnimation warning when we are not using CoreAnimation in our code.
CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces.
This occurs whenever we read/add/update/delete any record from CloudKit.
For example, here's our initial loading of Contacts into an NSTableView
-(void)loadContacts {
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"Contact" predicate:[NSPredicate predicateWithFormat:#"TRUEPREDICATE"]];
self.arrayToDisplay = [NSMutableArray new];
[self.publicDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
if ( error ) {
NSLog(#"error loading array: %#", error.localizedDescription);
} else {
self.arrayToDisplay.array = results; //arrayToDisplay is an array of CKRecord objects
[self.myTableView reloadData];
}
}];
}
self.publicDatabase is an initialized CKDatabase.
Note that all the functions work, read/add/update/delete, but that message appears after each call.
We have set the CA_DEBUG to log the backtraces and it appears it's got something to do with redrawing the NSTableView before the records are fetched?
This seems to fix the issue as it puts the tableView reload back onto the main thread. Just replace the [self.tableView reloadData] in the block with the following.
[self.tableView
performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:NO
];
performSelectorOnMainThread: will work just fine as you posted in your answer. However sometimes you want to do more than just execute a single method. In that case I recommend wrapping multiple lines of code into a GCD dispatch statement. Here's how it looks:
__weak UITableView *weakTableView; // use a weak reference to prevent retain cycle
dispatch_queue_t q_main = dispatch_get_main_queue();
dispatch_async(q_main, ^() {
[weakTableView reloadData];
// do other stuff too if you want
});

iOS - Parse multiple XML files for each tableView row

I have a UITableViewController.
I want to call a URL (http://webservices.company.nl/api?station=ut) multiple times (for each train station) where "ut" is always different (it's the code of the station). And I want to put the results each time in a new tableview row. (The URL returns XML).
To call the URL, I use this:
// Create connection
NSURLConnection *urlConnection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat: #"http://webservices.company.nl/api?station=%#", station.stationCode]]] delegate:self];
[urlConnection start];
Then in "connectionDidFinishLoading" I've this for parsing the URL content with NSXMLParser:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:receivedDataFromURL];
[parser setDelegate:self];
[parser parse];
}
I've implemented all the methods like "didStartElement", "didEndElement" and it successfully reads all the elements in the file.
My question:
What's the best way to do this for every row in my tableview and how can I put the results in every row?
I don't know what the best structure is for this, because I want to do this async.
Many thanks in advance.
The pattern here is just like lazy loading images.
1) Create a custom object like TrainStation, it should have an NSString station code, some BOOL property of function that tells callers that it's been initialized from the web service, and an init method that provides a block completion handler.
// TrainStation.h
#interface TrainStation : NSObject
#property (strong, nonatomic) NSString *stationCode; // your two character codes
#property (strong, nonatomic) id stationInfo; // stuff you get from the web service
#property (strong, nonatomic) BOOL hasBeenUpdated;
#property (copy, nonatomic) void (^completion)(BOOL);
- (void)updateWithCompletion:(void (^)(BOOL))completion;
#end
2) The completion handler starts an NSURLConnection, saving the completion block for later when the parse is done...
// TrainStation.m
- (void)updateWithCompletion:(void (^)(BOOL))completion {
self.completion = completion;
NSURL *url = // form this url using self.stationCode
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
}];
}
// TrainStation does it's own parsing, then
- (void)parserDidEndDocument:(NSXMLParser *)parser
self.hasBeenUpdated = YES;
self.completion(YES);
// when you hold a block, nil it when you're through with it
self.completion = nil;
}
3) The view controller containing the table needs to be aware that tableview cells come and go as they please, depending on scrolling, so the only safe place for the web result is the model (the array of TrainStations)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// normal stuff, dequeue cell, etc.
// the interesting part
TrainStation *trainStation = self.array[indexPath.row];
if ([trainStation hasBeenUpdated]) {
cell.detailTextLabel.text = [trainStation.stationInfo description];
// that's just a shortcut. teach your train station how to produce text about itself
} else { // we don't have station info yet, but we need to return from this method right away
cell.detailTextLabel.text = #"";
[trainStation updateWithCompletion:^(id parse, NSError *) {
// this runs later, after the update is finished. the block retains the indexPath from the original call
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
// this method will run again, but now trigger the hasBeenUpdated branch of the conditional
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
}
}];
}
return cell;
}
There are a few considerations:
You probably want to make each of these requests its own object so that you can have them running concurrently. The right approach is probably a custom operation for a NSOperationQueue to encapsulate the downloading and parsing of the XML. A couple of considerations here:
You should make this operation so it can operate concurrently.
You should make the operation respond to cancellation events.
Note, if you do your own NSOperation with a NSURLConnection with your own NSURLConnectionDataDelegate methods, you have to do some silliness with scheduling it in an appropriate run loop. I usually create a separate thread with its own runloop, but I see lots of people simply doing:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
You probably want to implement a caching mechanism:
At a minimum, you want to cache responses into memory (e.g. a NSCache) so that if you scroll down and then scroll back up, it doesn't need to reissue requests that it only just sent;
Depending upon the needs of your app, you might want a persistent storage cache, too. Maybe you don't in this particular situation, but it's a common consideration in these sorts of cases.
Given the network intense nature of your problem, I'd make sure you test your app in network realistic, real-world (and worst case) scenarios. On the simulator, you can achieve that with the "Network Link Conditioner" which is part of the "Hardware IO Tools" (available from the "Xcode" menu, choose "Open Developer Tool" - "More Developer Tools"). If you install the "Network Link Conditioner", you can then have your simulator simulate a variety of network experiences (e.g. Good 3G connection, Poor Edge connection, etc.).
Anyway, putting the this together, here is an example that is doing a XML request for every row (in this case, looking up the temperature for a city on Yahoo's weather service).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
CityCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// try to retrieve the cell from the cache
NSString *key = self.objects[indexPath.row];
City *cityFromCache = [self.cache objectForKey:key];
if (cityFromCache)
{
// if successful, use the data from the cache
cell.textLabel.text = cityFromCache.temperature;
cell.detailTextLabel.text = cityFromCache.name;
}
else
{
// if we have a prior operation going for this cell (i.e. for a row that has
// since scrolled off the screen), cancel it so the display of the current row
// is not delayed waiting for data for rows that are no longer visible;
// obviously, for this to work, you need a `weak` property for the operation
// in your `UITableViewCell` subclass
[cell.operation cancel];
// re-initialize the cell (so we don't see old data from dequeued cell while retrieving new data)
cell.textLabel.text = nil;
cell.detailTextLabel.text = nil;
// initiate a network request for the new data; when it comes in, update the cell
CityOperation *operation = [[CityOperation alloc] initWithWoeid:key successBlock:^(City *city) {
// see if the cell is still visible
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
// if the cell for this row is still visible, update it
if (updateCell)
{
updateCell.textLabel.text = city.temperature;
updateCell.detailTextLabel.text = city.name;
[updateCell setNeedsLayout];
}
// let's save the data in our cache, too
[self.cache setObject:city forKey:key];
}];
// in our custom cell subclass, I'll keep a weak reference to this operation so
// we can cancel it if I need to
cell.operation = operation;
// initiate the request
[self.queue addOperation:operation];
}
return cell;
}
In practice might move some of that logic into my cell subclass, but hopefully this illustrates the idea.
Having outlined an answer to your question, I must confess that when you described what you're trying to do, I immediately gravitated to radically different designs. E.g. I might kick off an asynchronous process that does a bunch of XML requests, updating a database, posting notification to my table view letting it know when data has been inserted. But this is a more radical departure from what you've asked, so I refrained. But it might be worthwhile to step back and consider the overall architecture.

ios loop for asynchronous requests

I have to make multiple http requests asynchronously (one request at a time). For looping I am doing this:
-(void) Foo1
{
[makerequest];
}
-(void) requestCompletes
{
//Do something
[self Foo1:[array objectAtIndex:i++]];
}
viewDidLoad
{
[self Foo1:[array objectAtIndex:0]];
}
But looping in the completion-handler doesn't seems a good idea to me. Is this the correct way?
No, this is not the correct way. The iOS platform has a NSOperationQueue class, which allows you to schedule operations. By creating you're own subclass of any of the operations (NSInvocationOperation, NSBlockOperation or the NSOperation) and using it to wrap around a NSURLRequest you can easily add and execute web request by calling
[operationQueue addOperation:requestOperation];

RestKit Handling overlapping RKRequest Delegates

I have several RestKit gets that all use the same format:
[[RKClient sharedClient] get:endString queryParameters:params delegate:self];
I have a masterMethod that essentially refreshes all my user's restful data that looks like this
-(void)masterMethod
{
[self get1];
[self get2];
[self get3];
[self get4];
[self get5];
}
Where all the gets are in the same format as the one above. All of this code is in a class that includes the delegate method:
- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response
However, I think something is going wrong when I try to call all give gets in the same method. It's as though the delegate didLoadResponse & didRecieveResponse methods are overlapping or getting release or something. Is there a way to make a master queue to handle this huge call? Or is something else going wrong.
I'm getting a BAD_ACCESS error somewhere in the masterMethod call.
Thanks, any help greatly appreciated.
What are you getting? If you're pulling down objects you should us the isKindOfClass method to distinguish the objects in objectLoader:didLoadObjects and set appropriately.
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
if ([[objects objectAtIndex:0] isKindOfClass:[Apple class]]) {
Apple *apple = [objects objectAtIndex:0];
}
else if ([[objects objectAtIndex:0] isKindOfClass:[Banana class]]) {
Banana *banana = [objects objectAtIndex:0];
}
}
If you're pulling data from the request response, look into setting userdata on the request object, then checking the userdata in request:didLoadResponse. For more information see RestKit: distinguish multiple requests in didLoadResponse:.

Crash after reseting NSPersistentStore while NSFetchedResultsController is tracking changed

Here's what I'm trying to do:
I use NSFetchedResultsController to perform a fetch and track changes using its delegate
I download some data and depending on some condition I sometimes delete all local data stored by CoreData by removing the NSPersistentStore and recreating a new one.
I create managed objects based on the data and save them
NSFetchedResultsController should now inform me that I have some changes
What I get instead is this crash when trying to save the data:
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Object's persistent store is not reachable from this NSManagedObjectContext's coordinator with userInfo (null)
I'm always using a single NSManagedObjectContext and I always read & save on the main thread.
It seems that switching the NSPersistenStore somehow messes up the fetched results controller. Is this expected behavior or am I doing something wrong?
I would not recommend this approach. I would create a new MOC with your new persistent store and let go of the old MOC.
I assume at some point you call -[ManagedObjectContext reset]? Before you do that, you have to let go of all managed objects that come from that context. They all become invalid (which is likely the cause of your crash).
You should also take a look at How to force coredata to rebuild sqlite database model?.
I had the same crash. Following Rob's suggestion, I additionally posted an NSNotification each time I called removePersistentStore: - and all my ViewControllers that have an NSFetchedResultController now automatically nullify their local NSFetchedResultsController when that happens.
i.e.:
1 - LISTEN TO NOTIFICATION:
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Custom initialization
[[NSNotificationCenter defaultCenter] addObserverForName:kNotificationDestroyAllNSFetchedResultsControllers object:nil queue:nil usingBlock:^(NSNotification *note) {
NSLog(#"[%#] must destroy my nsfetchedresultscontroller", [self class]);
[__fetchedResultsController release];
__fetchedResultsController = nil;
}];
}
return self;
}
2 - POST NOTIFICATION
for( NSPersistentStore* store in [self.persistentStoreCoordinator persistentStores] )
{
NSError *error;
NSURL *storeURL = store.URL;
[self.persistentStoreCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error];
/** ... side effect: all NSFetchedResultsController's will now explode */
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDestroyAllNSFetchedResultsControllers object:self];
}
The NSFetchedResultsController monitors changes to objects in its associated managed object context thus it might be monitoring changes to objects in an obsolete managed object context.
This issue got fixed when I recreated the NSFetchedResultsController after resetting the NSPersistentStore