Variable use outside of Block sentense - iphone

This is the question about Block sentence. My question is how to get variable in block sentence out. Please watch my code. The code below doesn't works well.
__block NSURL *ssURL
AFJSONRequestOperation *operation =
[AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
ssURL = [NSURL URLWithString:[JSON valueForKeyPath:#"screenshot"]];
[self currentEntry].ogImageURL = ssURL;
NSLog(#"%#", ssURL); // -> "http://correct.address"
} failure:nil];
[operation start];
Also I set the variable ogImageURL as below:
#property (copy, atomic) NSURL *ogImageURL;
Out of completion block, the NSLog shows "(null)" obviously. I want to use the variable outside of block sentence. This is strange.
I have to get the variable from outside of the block because I want to create the table view in which cell calls to get self information to the array.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = nil;
if (indexPath.row < self.itemsArray.count) {
cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
UIImageView *imageViewOgImage = (UIImageView *)[cell viewWithTag:5];
NSURL *ogImageURL = [[self.itemsArray objectAtIndex:indexPath.row] ssURL];
The full source is the 186th line of this: https://github.com/weed/p120711_TechMovie/blob/120723/TechMovie/RSSParser.m

This is not a problem of scope, it is a timing issue
in the first example the image view is setup inside the block, which means it only ever happens after the ssURL value has been set
in the second example, the block is created but is not called before the image view is setup. The block is asynchronous so is not called immediately: it is run when a response is returned from the operation. If you are setting up the image view based on the JSON request operation it must be done after the result of the operation is set (i.e. inside the completion block)
If you're tableview depends on the result of the operation, then set the value of ssURL and then inside the block, reload the table view

I'm assuming AFJSONRequestOperation JSONRequestOperationWithRequest is an asynchronous method, in which case in the second example you'll be setting self.imageView to whatever value is in self.ssURL at the time you dispatch the JSONRequestOperation. You'll have to set the image in the successful completion block, as you did in the first example.

Related

Downloading thumbnail to UITableViewCell

I have a UITableView that populates its cells with thumbnail images. In my case, the thumbnails are downloaded from a server. My code is as follows:
if (![self thumbnailExists])
{
self.thumbnailImageView.image = nil;
[self.activityIndicatorView startAnimating];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.thumbnailURL]];
UIImage *image = [UIImage imageWithData:data];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
SubmenuScrollViewTableViewCell *submenuScrollViewTableViewCell = (SubmenuScrollViewTableViewCell*)[(UITableView*)self.superview cellForRowAtIndexPath:self.indexPath];
[submenuScrollViewTableViewCell.activityIndicatorView stopAnimating];
submenuScrollViewTableViewCell.thumbnailImageView.image = image;
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[self title]];
}
This code is based on the great WWDC2012 presentation: "Building Concurrent User Interfaces on iOS". The main difference between my code and the code in the presentation is I'm populating my cells with data that is retrieved from the web.
It works well for the most part, but the problem I'm having is if I scroll really fast, then one of the cells might show up with the wrong thumbnail (usually I noticed said cell would be a multiple of the cell from which the thumbnail belongs to). I should mention this code is being executed in the layoutSubviews method of an overridden UITableViewCell.
Can anyone spot any flaws in this code?
The reason your code is breaking is because of this line of code:
SubmenuScrollViewTableViewCell *submenuScrollViewTableViewCell = (SubmenuScrollViewTableViewCell*)[(UITableView*)self.superview cellForRowAtIndexPath:self.indexPath];
You are grabbing the cell at the index path when this is called and placing it in whatever cell is at this index path when the cell is reused. This puts a different image in a different cell when the view moves very fast and perhaps the response from the server comes in at a different time.
Make sure you cancel the operation in prepareForReuse and clear out the cell image so that it does not place the image in the wrong cell, or an image does not get re-used.
Also, it is generally bad practice to call the tableView from the cell itself.
The issue had to do with the fact that the above code was in the layoutSubviews method of a subclassed UITableViewCell I created. When I moved it back in the cellForRowAtIndexPath method, the problem was eliminated.

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.

AFNetworking setImageWithURLRequest reverting to placeholder when scrolled past

I am making a simple reddit app for a school project. I am loading my
data from reddit via json (http://www.reddit.com/.json for example) with AFNetworking library.
I am displaying each reddit thread with a prototype cell, which
contains a UIImageView for the post thumbnails.
I am trying to use AFNetworking to lazy load the images, with the
setImageWithURLRequest method.
the problem: when the app launches all the thumbnails load lazily as they should as I scroll down the tableView. As soon as the cell is out of the view and I scroll back up to it, the thumbnail has been replaced with the placeholder image -- even though it loaded the correct thumbnail before scrolling past.
Relevant code from cellForRowAtIndexPath method. the lazy loading is being called in the setImageWithURLRequest block
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"threadCell";
SubredditThreadCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSDictionary *tempDictionary = [self.subredditArrayFromAFNetworking objectAtIndex:indexPath.row];
NSDictionary *singleThreadDict = [tempDictionary objectForKey:#"data"];
if ( [[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"nsfw"] ){
cell.postImageThumbnail.image = [UIImage imageNamed:#"NSFWIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"self"]){
cell.postImageThumbnail.image = [UIImage imageNamed:#"selfIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] length] == 0){
cell.postImageThumbnail.image = [UIImage imageNamed:#"genericIcon"];
}
else{
NSURL *thumbURL = [NSURL URLWithString:[singleThreadDict objectForKey:#"thumbnail"] ];
[cell.postImageThumbnail setImageWithURLRequest:[NSURLRequest requestWithURL:thumbURL]
placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
if (request) {
[UIView transitionWithView:cell.postImageThumbnail
duration:1.0f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
[cell.postImageThumbnail setImage:image];
}
completion:NULL];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
NSLog(#"failure loading thumbnail");
}
];
}
return cell;
}
Don't directly update the cell's postImageThumbnail when the image is done downloading.
Instead, tell the UITableView to refresh just that cell, using – reloadRowsAtIndexPaths:withRowAnimation:.
To get better performance, UITableView will re-use a cell-object once it's been scrolled offscreen, to show different data that is currently visible. (This is why dequeueReusableCellWithIdentifier: starts with "dequeueReusable", instead of "makeNew".) This lets UITableView only create about as many cells as are visible, instead of having to create and destroy as many cells as there are rows in the table. By the time your networking request succeeds, the cell that the success: block captures is being used to display another row, and you're over-writing that row's image.

downloading image is slow even after using Async call in iphone

I have to 20-25 download images of 50 Kb- 2 Mb each and show them in a tableview.
I used ASIHTTPRequest asyn request to this. I observed that after some time the app gets stuck. This should not happen because I am using a async call. I thought something is wrong with ASIHTTPRequest and I observed that The didFinished selector gets called in the main thread. The only thing which I do is
-(void)didFinishedDownloadingImage:(ASIHTTPRequest*)request
{
NSData *responseData = [request responseData];
UIImage *image = [UIImage imageWithData:responseData];
[[data objectAtIndex:request.tag] setImage:image];
[self.tableView reloadData];
}
I don't think this should cause any problem. Also in cellforrowatindexpath I just do
- (UItableViewCell *)tableviewView:(UItableView *)tableview
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UserProfile * user = [data objecAtIndex:indexpath.row];
UITableViewCell *cell = [tableView
dequeueReusableCellWithReuseIdentifier:#"ProfileCell"
forIndexPath:indexPath];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewDefaultStyle];
}
NSString *fullname = [NSString stringWithFormat:#"%#\n%#",
user.firstname, user.lastname];
if(user.image != nil)
[cell.imageView setImage:user.image];
else{
[cell.imageView setImage:[UIImage imageNamed:#"placeholder.jpg"]];
}
[cell.label setText:fullname];
return cell;
}
But the app is slow and freezes for 1-2 sec which is a considerable amount of time.
I have seen apps which does this very smoothly. I tried using an image of fixed size 5Kb which has a very significance performance improvement with using the above code. I don't know why should that make a difference for big images in this case because all downloading is happening in other thread via ASIHTTP .
Please, replace your framework with AFNetworking.
You can simple use..
IImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
[imageView setImageWithURL:[NSURL URLWithString:#"http://i.imgur.com/r4uwx.jpg"] placeholderImage:[UIImage imageNamed:#"placeholder-avatar"]];
or... directly in TableViewCell
NSURL *url = [[NSURL alloc] initWithString:[movie objectForKey:#"artworkUrl100"]];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"placeholder"]];
"In the second line, we tell the image view where the thumbnail is located by passing an NSURL and we pass in a placeholder image, which is shown as long as our request has not returned a response"
Thats all!
Here you have an tutorial about that http://mobile.tutsplus.com/tutorials/iphone/ios-sdk_afnetworking/
It's easy to make assumptions about the root cause of a laggy/slow application. Instead of guessing, why don't you test your suspicions? Profile your application with the Time Profiler instrument. It'll tell you which methods and functions your application is spending the most time in.
Here are some ideas until you have a chance to profile:
You might consider downloading the full-res images and creating thumbnails in the background and then caching them in an NSCache object. You can also run [UIImage imageWithData:responseData]; in a background thread. It's thread-safe until the point at which it interacts with the view hierarchy.
Selectively reloading a single cell should be faster than reloading the entire tableview, especially one with lots of images. Furthermore if you're doing all of the networking and processing on a background queue, there's no reason scrolling the tableview should be slow. Can you show us your entire implementation of the -cellForRowAtIndexPath: method? You've mentioned that you think setImage: is your slow point because rendering is slow. If you reload a single cell, only one cell needs to be rendered. If you reload the entire tableview, every cell must be re-rendered.

How do we sync numberOfRowsInSection to cellForRowAtIndexPath?

Our UITableView returns the number of rows for a given section. What we're experiencing is by the time cellForRowAtIndexPath is called, the number has changed so we end up getting array index out of bounds.
Is there a good way of syncing these two methods so the underlying data is not changed? We considered using #synchronized, but not sure when you would release the lock.
One other thing we're doing to refresh the table is this from a separate thread.
[self addUsers:usersToShow];
[[self invokeOnMainThreadAndWaitUntilDone:NO] refresh:self]; // is this the issue?
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.users.count; // returns 10 for example
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *newCell= nil;
static NSString *cellId = #"cellId";
cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"MyCell" owner:self options:nil];
cell = newCell;
self.newCell = nil;
}
User* user = [self.users objectAtIndex:indexPath.row]; // index out of bounds now because number of users has changed
}
As you've kind of worked out for yourself, you can't use #synchronized because the scope extends beyond the method.
You don't want to be trying to use a Lock object, locking it in numberOfRowsInSection and unlocking it cellForRowAtIndexPath. It's not going to work. What you need to do is ensure that you do any locking you need to in cellForRowAtIndexPath and handle the fact that the row passed in might not be valid anymore e.g.:
User * user = nil;
#synchronized(self.users) {
if (indexPath.row < [self.users count]) {
user = [self.users objectAtIndex:indexPath.row];
}
}
if (user) {
//configure cell
}
else {
//set cell fields to be blank
}
Have you tried updating the model (self.users) only in the main thread? This should reduce the likely hood of your updates to the model from interleaving with calls to getNumberOfRows and configureCellAt. In the example you've given you are updating the model on a random thread and then reloading the data on the main thread. I'd recommend ensuring that you're model is thread safe (or updated/read in only the main thread).
Even though brain has already answered, I want to emphasize "update model in main thread" with example code.
You might encounter the problem because your model changed in some background thread. The timeline should look like this:
{NSThread number = 1, name = main} -[ViewController tableView:numberOfRowsInSection:] (return 10 for example)
{NSThread number = 8, name = (null)} -[ViewController changeTheModel] (remove some objects from model, or get a new model with less than 10 objects)
{NSThread number = 1, name = main} -[ViewController tableView:cellForRowAtIndexPath:] (get the index out of bound exception because 10th object does not exist)
To solve the problem, you should do something like this when you change your model:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray* items = [self getNewModel];// get new model on background thread
dispatch_async(dispatch_get_main_queue(), ^ {
self.items = items;// replace the model with new one on main thread
[self.tableView reloadData];// refresh table without index out of bound exception
});
});
Hope this could help you. :)