SO I have a program which calls the FlickR API, gets the URL's puts them into a dictionary and then assigns them into a table view, using an image view.
NSArray *photos = [self.flickr photosForUser:#"James Kinvig"];
int countAttempts = 0;
[[self.flickr photosForUser:#"James Kinvig"]count];
for (int i = 0; i < [[self.flickr photosForUser:#"James Kinvig"]count]; i++) {
for(NSDictionary *dictionary in photos) {
countAttempts++;
NSString *farmId = [dictionary objectForKey:#"farm"];
NSString *serverId = [dictionary objectForKey:#"server"];
NSString *photoId = [dictionary objectForKey:#"id"];
NSString *secret = [dictionary objectForKey:#"secret"];
self.url= [NSURL URLWithString:[NSString stringWithFormat:#"http://farm%#.staticflickr.com/%#/%#_%#.jpg", farmId, serverId, photoId, secret]];
//NSLog(#"self.url = %#", self.url);
NSLog(#"count = %d", countAttempts);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:self.url];
dispatch_sync(dispatch_get_main_queue(), ^{
UIImage *img = [UIImage imageWithData:imgData];
cell.imageView.image = img;
[cell setNeedsLayout];
});
});
}
}
return cell;
}
This is the method it calls, photosForUser:
- (NSMutableArray *) photosForUser: (NSString *) friendUserName
{
NSString *request = [NSString stringWithFormat: #"https://api.flickr.com/services/rest/?method=flickr.people.findByUsername&username=%#", friendUserName];
NSDictionary *result = [self fetch: request];
NSString *nsid = [result valueForKeyPath: #"user.nsid"];
request = [NSString stringWithFormat: #"https://api.flickr.com/services/rest/?method=flickr.photos.search&per_page=%ld&has_geo=1&user_id=%#&extras=original_format,tags,description,geo,date_upload,owner_name,place_url", (long) self.maximumResults, nsid];
result = [self fetch: request];
return [result valueForKeyPath: #"photos.photo"];
}
Which does a fetch to the flickr API.
What is happening though is that is stuck in an eternal loop. Even with the for statement being less than the count, it still eternal loops. I have NSLog'd the count of the FlickR photos and it = 11.
This may have something to do with it, but whenever I press the button to take me to the table view controller, I get a HUGE lag, close to a minute, and nothing is being calculated (photo-wise) as I've done a count++
Thanks
let me understand this.. By the last line of your first block of code, I conclude that that is the uitableview dataSource method, cellForRowAtIndexPath.. what doesn't really makes sense.. you you have a fetch there, you have A loop inside a loop, that is setting many images (by download them) in one single imageView, and this is happening for all your visible cells at the same time. This will never work!
The solution is:
1 - remove this method from the cellForRow, this is not the place to request the images
2 - create another method that will fetch the content
3 - create a method that will do your loops and store the images on the array so you don't need to do that many times, only one..
4 - reload the tableview after you finish the step 3
5 - use the array of images that is already done to set your images by indexPath.row in your cell..
6 - I recommend you to use a Library for imageCache (i.e https://github.com/rs/SDWebImage)
NSArray *photos = [self.flickr photosForUser:#"James Kinvig"];
for (int i = 0; i < [[self.flickr photosForUser:#"James Kinvig"] count]; i++)
{
for (NSDictionary *dictionary in photos)
{
You have two nested loops iterating over the same collection. This turns what should be an O(n) operation into O(n^2) and explains why your process is taking a very long time.
Since the loop bodies never use i, I would fix it by getting rid of the outer loop:
NSArray *photos = [self.flickr photosForUser:#"James Kinvig"];
for (NSDictionary *dictionary in photos)
{
Related
i have created a UITableView which populates each cell from URL requests. I have used 'dispatch_queue' to prevent the UItableView from freezing. For some reason when i scroll through the UITableView the images flash and disappear and fill the wrong cell for a second until fixing themselves. Here is my code. I am pulling the feed using restkit
customCell.customCellTextLabel.text = [NSString stringWithFormat:#"%#",feedO.title];
NSString *string = [NSString stringWithFormat:#"%#",feedO.body];
NSURL *urlString;
NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSArray *matches = [linkDetector matchesInString:string options:0 range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypeLink) {
urlString = [match URL];
NSLog(#"found Body URL: %# and title %#", urlString,feedO.title);
}
}
dispatch_queue_t imageQueue = dispatch_queue_create("imageDownloader",nil);
dispatch_async(imageQueue, ^{
NSData *data = [[NSData alloc] initWithContentsOfURL:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
customCell.customCellImageView.image = [UIImage imageWithData: data];
});
});
return customCell;
You should consider using this library SDWebImage, available here https://github.com/rs/SDWebImage for that kind of problems. It handles asynchronous download and cache for remote images very easily.
The simpliest installation is done by using CocoaPods
CocoaPods (http://cocoapods.org) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries in your projects.
See the Get Started section for more details.
Use in your Podfile
platform :ios, '6.1'
pod 'SDWebImage', '~>3.6'
After installing the dependency for SDWebImage, just simply use the following lines in your view controller :
#import <SDWebImage/UIImageView+WebCache.h>
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
your code was missing one line , that i added.
customCell.customCellImageView.image =nil;
customCell.customCellTextLabel.text = [NSString stringWithFormat:#"%#",feedO.title];
NSString *string = [NSString stringWithFormat:#"%#",feedO.body];
NSURL *urlString;
customCell.customCellImageView.image =nil;
NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSArray *matches = [linkDetector matchesInString:string options:0 range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypeLink) {
urlString = [match URL];
NSLog(#"found Body URL: %# and title %#", urlString,feedO.title);
}
}
dispatch_queue_t imageQueue = dispatch_queue_create("imageDownloader",nil);
dispatch_async(imageQueue, ^{
NSData *data = [[NSData alloc] initWithContentsOfURL:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
customCell.customCellImageView.image = [UIImage imageWithData: data];
});
});
return customCell;
The reason for this is the UITableView is reusing the cells created.
So firstly it creates lets say 10 of your CustomCell class.
Cell 0
Cell 1
...
Cell 9
By creating these 10 cells it will initiate 10 async calls to fetch images.
block 0 -> Cell 0
block 1 -> Cell 1
...
block 9 -> Cell 9
This will work fine if you don't scroll until all 10 downloads have finished.
But as soon as you start scrolling, the UITableView will start reusing the cells created and initiate new downloads.
So first it might reuse Cell 0 and create block 10 -> Cell 0.
If block 0 was not finished when Cell 0 was picked for reuse, you will now have two blocks wanting to put their image onto the same cell.
Leading to the following two scenarios:
Block 0 finishes first, then block 10
Block 10 finishes first, then block 0
This is what is causing the "flashing".
Then imagine scrolling through 1000s of cells within seconds :)
Solution
You need to be able to cancel the queued block for your cell being reused.
I would use e.g. SDWebImage or FastImageCache.
How you said. You catch the image in another thread, in order to prevent freezing. And in a tableView the cells are reused, and while in one background thread you are catching the new photo, in the main thread the cell with the previous photo is returned.
Here in the code I fix it and better explanation:
customCell.customCellTextLabel.text = [NSString stringWithFormat:#"%#",feedO.title];
NSString *string = [NSString stringWithFormat:#"%#",feedO.body];
NSURL *urlString;
NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSArray *matches = [linkDetector matchesInString:string options:0 range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypeLink) {
urlString = [match URL];
NSLog(#"found Body URL: %# and title %#", urlString,feedO.title);
}
}
// Here you need, at least quit the previous imagen, is better if you implement
// and activity indicator (here start), and also you should implement some imagen cache.
customCell.customCellImageView.image = nil;
// Apart from here, go to another thread.
dispatch_queue_t imageQueue = dispatch_queue_create("imageDownloader",nil);
dispatch_async(imageQueue, ^{
// this operation is slowly that return a cell, this happens after [1]
NSData *data = [[NSData alloc] initWithContentsOfURL:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
//If you implement and activity indicator stop here.
customCell.customCellImageView.image = [UIImage imageWithData: data];
});
});
// [1] the cell is returned before the image is ready.
return customCell;
Above answer is correct. But no need to download image in cell for row. you can download image before table loading occurs in viewWillAppear or ViewDidLoad which fits to your requirement.
When you scroll your table every time your image will download again. to prevent this i prefer you to get image from cache and set in your imageview directly. so as per your code. if you scroll from 0 index to 10 then cell will download image for all 10 cells. after that if you scroll again 10 to index 0 . then image will be download again.
you can use (https://github.com/rs/SDWebImage) to download image async. its very easy and fast.
i prefered this library because it will handle your cache. just write below code
#import "UIImageView+WebCache.h"
// in cellForRowAtIndexPath.
[customCell.customCellImageView sd_setImageWithURL: [NSURL URLWithString:urlString]];
remove below code.
dispatch_queue_t imageQueue = dispatch_queue_create("imageDownloader",nil);
dispatch_async(imageQueue, ^{
NSData *data = [[NSData alloc] initWithContentsOfURL:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
customCell.customCellImageView.image = [UIImage imageWithData: data];
});
});
Maybe this will help you.
I am using an NSMutableArray to store items in a list form from a web service. How do I read the data out of my array to display a random quote.
As i am trying to avoid hitting the webservice twice or am i over complicating it?
NSInteger randomInt = arc4random_uniform(3);
NSString *baseUrl = #"http://movie-quotes.herokuapp.com/api/v1/quotes";
NSString *webServiceUrl= [NSString stringWithFormat:#"%#/%ld", baseUrl,(long)randomInt];
randomQuotes = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// Load the JSON string from our web serivce (in a background thread)
NSDictionary * dictionary = [JSONHelper loadJSONDataFromURL:webServiceUrl];
dispatch_async(dispatch_get_main_queue(), ^{
randomQuotes = [NSMutableArray array];
// Iterate through the array of quotes records in the dictionary
for (NSDictionary * oneQuote in dictionary)
{
Quotes* newQuotes =[[Quotes alloc] init];
// Add our new Customer record to our NSMutableArray
newQuotes.Quote = [oneQuote objectForKey:#"content"];
newQuotes.FilmName = [oneQuote objectForKey:#"film"];
[randomQuotes addObject:newQuotes];
}
});
});
You can get a random object out of an array like this.
NSUInteger randomIndex = arc4random_uniform(theArray.count);
[theArray objectAtIndex: randomIndex];
As long as you keep the array of quotes around, you won't need to hit the web service again.
I'm trying to use NSData to pull information out of a text file, and then load it into a dictionary.
First I create a string of the text file, and load each record into an array.
Then I break apart the each record into individual data elements.
The problem I'm having is that when the dictionary is fully populated, I then use addObject to load it into the array, which it does do successfully. The problem is that when the next loop creates a new dictionary, the same dictionary gets loaded into the array, and I end up an array of all the same dictionaries, instead of multiple different dictionary objects.
I'm guessing there is some simple mistake I'm making that is causing this error. Any help would be appreciated.
NSString *clientListFile = [NSURL URLWithString: #"/textfile"];
NSData *clientListDataFile = [NSData dataWithContentsOfFile:clientListFile];
NSString *clientListString = [[NSString alloc]initWithBytes:[clientListDataFile bytes] length:[clientListDataFile length] encoding:NSUTF8StringEncoding];
NSString *returnDelimiter = #"\n";
NSString *commaDelimiter = #",";
NSString *exclamationDelimiter = #"!";
NSArray *keysAndObjects = [[NSArray alloc]init];
NSMutableDictionary *clientList = [[NSMutableDictionary alloc]init];
NSMutableArray *clientListOfDictionaries = [[NSMutableArray alloc]init];
NSArray *sentenceArray = [clientListString componentsSeparatedByString:returnDelimiter];
for (int i = 0; i < [sentenceArray count]; i=i+1) {
[clientList removeAllObjects]; //to start with a fresh dictionary for the next iteration
NSString *recordSentence = [sentenceArray objectAtIndex:i];
NSArray *attributes = [recordSentence componentsSeparatedByString:commaDelimiter];
for (int j = 0; j < [attributes count]; j = j+1) {
NSString *pairsOfItems = [attributes objectAtIndex:j];
//a small arry, of only two objects, the first is the key, the second is the object
keysAndObjects = [pairsOfItems componentsSeparatedByString:exclamationDelimiter];
[clientList setObject:[keysAndObjects lastObject] forKey:[keysAndObjects firstObject]];
}
[clientListOfDictionaries addObject:clientList];
}
When I used NSLog to see what's in the dictionary, I mulitple objects of the same dictionary repeated, even though up earlier in the iteration, I can see that the code is creating separate and unique dictionaries.
Instead of this line
[clientListOfDictionaries addObject:clientList];
you can have
[clientListOfDictionaries addObject:[[NSArray alloc] initWithArray:clientList];
That way you will be adding new arrays to clientListOfDictionaries instead of the same one.
Move this line:
NSMutableDictionary *clientList = [[NSMutableDictionary alloc]init];
to just after the first for loop line and then delete the line:
[clientList removeAllObjects];
It's important to create a new dictionary for each iteration.
You should also delete the following line:
NSArray *keysAndObjects = [[NSArray alloc]init];
and change:
keysAndObjects = [pairsOfItems componentsSeparatedByString:exclamationDelimiter];
to:
NSArray *keysAndObjects = [pairsOfItems componentsSeparatedByString:exclamationDelimiter];
You are allocated and initialising your clientList dictionary outside of the for loop, so you only have one dictionary, which you are storing in your array multiple times. Adding the dictionary to the array does not copy it, it merely adds a pointer to the object.
you need to move
NSMutableDictionary *clientList = [[NSMutableDictionary alloc]init];
inside your first for loop in place of
[clientList removeAllObjects];
Also, componentsSeparatedByString: returns an NSArray, so you don't need to allocate and initialise one. You can simply define the variable -
NSArray *keysAndObjects;
Because you're using the same clientList variable for each iteration of the loop. You need to create a whole new dictionary object each time.
Try this modified code:
NSData *clientListDataFile = [NSData dataWithContentsOfFile:clientListFile];
NSString *clientListString = [[NSString alloc]initWithBytes:[clientListDataFile bytes] length:[clientListDataFile length] encoding:NSUTF8StringEncoding];
NSString *returnDelimiter = #"\n";
NSString *commaDelimiter = #",";
NSString *exclamationDelimiter = #"!";
NSArray *keysAndObjects = nil;
NSMutableArray *clientListOfDictionaries = [[NSMutableArray alloc] init];
NSArray *sentenceArray = [clientListString componentsSeparatedByString:returnDelimiter];
for (NSUInteger i = 0; i < [sentenceArray count]; ++i) {
NSMutableDictionary *clientList = [[NSMutableDictionary alloc] init]; //to start with a fresh dictionary for the next iteration
NSString *recordSentence = [sentenceArray objectAtIndex:i];
NSArray *attributes = [recordSentence componentsSeparatedByString:commaDelimiter];
for (NSUInteger j = 0; j < [attributes count]; ++j) {
NSString *pairsOfItems = [attributes objectAtIndex:j];
//a small arry, of only two objects, the first is the key, the second is the object
keysAndObjects = [pairsOfItems componentsSeparatedByString:exclamationDelimiter];
[clientList setObject:[keysAndObjects lastObject] forKey:[keysAndObjects firstObject]];
}
[clientListOfDictionaries addObject:clientList];
}
An alternate option, though likely less efficient, is to to change the line:
[clientListOfDictionaries addObject:clientList];
to
[clientListOfDictionaries addObject:[clientList copy]];
That lets you keep using the same clientList variable, since you're adding a copy of it to the clientListOfDictionaries array. I just point that out because it might help you understand what's going on.
Also, note that I changed this line for you:
NSArray *keysAndObjects = [[NSArray alloc]init];
to
NSArray *keysAndObjects = nil;
Because it's just a pointer that is set by your call to componentsSeparatedByString, you don't need to allocate an array for it. That array will just vanish in your first iteration of the loop.
Should be added the new dictionary to array. Otherwise it will not add to an array. Every object in array have same dictionary mapping. So it will give you the same dictionary value. Create new dictionary for every object and add to array.
for (int i = 0; i < [sentenceArray count]; i=i+1) {
NSMutableDictionary *clientList = [[NSMutableDictionary alloc]init];
NSString *recordSentence = [sentenceArray objectAtIndex:i];
NSArray *attributes = [recordSentence componentsSeparatedByString:commaDelimiter];
for (int j = 0; j < [attributes count]; j = j+1) {
NSString *pairsOfItems = [attributes objectAtIndex:j];
//a small arry, of only two objects, the first is the key, the second is the object
NSArray *keysAndObjects = [pairsOfItems componentsSeparatedByString:exclamationDelimiter];
[clientList setObject:[keysAndObjects lastObject] forKey:[keysAndObjects firstObject]];
}
[clientListOfDictionaries addObject:clientList];
}
I have a method which downloads some pics from a server. I had the buffer for the downloaded data defined withing async block (NSMutableArray *imgtmp) but haven't figured out how to get the array back out of there. What's the best way to access imgtmp in order to return it's contents, or set an instance variable from it?
I've been looking in the Apple Block docs but I must not be in the right section. Do I need to declare imgtmp using the __block keyword somehow? I tried that but imgtmp was still empty outside the block. Thanks!
EDIT: code updated with working model
- (void) loadImages
{
// temp array for downloaded images. If all downloads complete, load into the actual image data array for tablerows
__block NSMutableArray *imgtmp = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0),
^{
int error = 0;
int totalitems = 0;
NSMutableArray *picbuf = [[NSMutableArray alloc] init];
for (int i=0; i < _imageURLS.count;i++)
{
NSLog(#"loading image for main image holder at index %i",i);
NSURL *mynsurl = [[NSURL alloc] initWithString:[_imageURLS objectAtIndex:i]];
NSData *imgData = [NSData dataWithContentsOfURL:mynsurl];
UIImage *img = [UIImage imageWithData:imgData];
if (img)
{
[picbuf addObject:img];
totalitems++;
}
else
{
NSLog(#"error loading img from %#", [_imageURLS objectAtIndex:i]);
error++;
}
}// for int i...
dispatch_async(dispatch_get_main_queue(),
^{
NSLog(#"_loadedImages download COMPLETE");
imgtmp = picbuf;
[_tvStatus setText: [NSString stringWithFormat:#"%d objects have been retrieved", totalitems]];
NSLog (#"imgtmp contains %u images", [imgtmp count]);
});// get_main_queue
});// get_global_queue
}
You're hitting that "final" NSLog call before any of your block code has executed. All your image loading stuff is wrapped in a dispatch_async. It is executed asynchronously whereas the NSLog is called right away.
I think the best thing for you to do is to pass imgtmp along to some persistent object. Perhaps your view controller can have a property like:
#property (nonatomic, copy) NSArray *images;
and you can assign that in the same block where you assign the text to _tvStatus.
I have read a few posts and found a method. I adapted it to my app, however at [self saveContext]; I get a bad instruction error.
Please let me know why, and if this method makes sense. Also I am using \r because in the csv each item is in its own row.
Thank you in advance
-(void)addData{
NSString *paths = [[NSBundle mainBundle] resourcePath];
NSString *bundlePath = [paths stringByAppendingPathComponent:#"coredatainfo.csv"];
NSString *dataFile = [[NSString alloc] initWithContentsOfFile:bundlePath];
NSArray *dataRows = [dataFile componentsSeparatedByString:#"\r"];
[dataFile release];
FirstCDitem *myItem;
for (int i = 0 ; i < [dataRows count] ; i++)
{
NSArray *dataElements = [[dataRows objectAtIndex:i] componentsSeparatedByString:#","];
NSLog(#"Added: %d %#",i,dataElements);
myItem = (FirstCDitem *)[NSEntityDescription insertNewObjectForEntityForName:#"FirstCDitem" inManagedObjectContext:[self managedObjectContext]];
[FirstCDitem setTitle:[dataElements objectAtIndex:i]];
[self saveContext];
}
}
There are two problems in this code line:
[FirstCDitem setTitle:[dataElements objectAtIndex:i]];
i is the current row number, so it probably makes no sense to use it as index to the dataElements array. Perhaps you want the first element in the row?
setTitle must sent to the instance myItem, not to the class FirstCDItem.
So the line should look like this:
[myItem setTitle:[dataElements objectAtIndex:0]];