I have an app that shows twitter account feed. So I have ImageView, textLabel and detailLabel for the content of the feed. The problem is that when all the data is loaded, the uiimage doesn't appear. When I click on the cell or scroll up-down, images are set. here is some of my code.
-(void)getImageFromUrl:(NSString*)imageUrl asynchronouslyForImageView:(UIImageView*)imageView andKey:(NSString*)key{
dispatch_async(dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:imageUrl];
__block NSData *imageData;
dispatch_sync(dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
imageData =[NSData dataWithContentsOfURL:url];
if(imageData){
[self.imagesDictionary setObject:[UIImage imageWithData:imageData] forKey:key];
dispatch_sync(dispatch_get_main_queue(), ^{
imageView.image = self.imagesDictionary[key];
});
}
});
});
}
- (void)refreshTwitterHomeFeedWithCompletion {
// Request access to the Twitter accounts
ACAccountStore *accountStore = [[ACAccountStore alloc] init];
ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
[accountStore requestAccessToAccountsWithType:accountType options:nil completion:^(BOOL granted, NSError *error){
if (granted) {
NSArray *accounts = [accountStore accountsWithAccountType:accountType];
// Check if the users has setup at least one Twitter account
if (accounts.count > 0)
{
ACAccount *twitterAccount = [accounts objectAtIndex:0];
NSLog(@"request.account ...%@",twitterAccount.username);
NSURL* url = [NSURL URLWithString:@"https://api.twitter.com/1.1/statuses/home_timeline.json"];
NSDictionary* params = @{@"count" : @"50", @"screen_name" : twitterAccount.username};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:url parameters:params];
request.account = twitterAccount;
[request performRequestWithHandler:^(NSData *responseData,
NSHTTPURLResponse *urlResponse, NSError *error) {
if (error)
{
NSString* errorMessage = [NSString stringWithFormat:@"There was an error reading your Twitter feed. %@",
[error localizedDescription]];
NSLog(@"%@",errorMessage);
}
else
{
NSError *jsonError;
NSArray *responseJSON = [NSJSONSerialization
JSONObjectWithData:responseData
options:NSJSONReadingAllowFragments
error:&jsonError];
if (jsonError)
{
NSString* errorMessage = [NSString stringWithFormat:@"There was an error reading your Twitter feed. %@",
[jsonError localizedDescription]];
NSLog(@"%@",errorMessage);
}
else
{
NSLog(@"Home responseJSON..%@",(NSDictionary*)responseJSON.description);
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData:responseJSON];
});
}
}
}];
}
}
}];
}
-(void)reloadData:(NSArray*)jsonResponse
{
self.tweets = jsonResponse;
[self.tableView reloadData];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return self.tweets.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
SNTwitterCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(!cell)
{
cell = [[SNTwitterCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
NSDictionary *tweetDictionary = self.tweets[indexPath.row];
NSDictionary *user = tweetDictionary[@"user"];
NSString *userName = user[@"name"];
NSString *tweetContaint = tweetDictionary[@"text"];
NSString* imageUrl = [user objectForKey:@"profile_image_url"];
[self getImageFromUrl:imageUrl asynchronouslyForImageView:cell.imageView andKey:userName];
cell.profileImage.image = [UIImage imageNamed:@"images.png"];
NSArray *days = [NSArray arrayWithObjects:@"Mon ", @"Tue ", @"Wed ", @"Thu ", @"Fri ", @"Sat ", @"Sun ", nil];
NSArray *calendarMonths = [NSArray arrayWithObjects:@"Jan", @"Feb", @"Mar",@"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec", nil];
NSString *dateStr = [tweetDictionary objectForKey:@"created_at"];
for (NSString *day in days) {
if ([dateStr rangeOfString:day].location == 0) {
dateStr = [dateStr stringByReplacingOccurrencesOfString:day withString:@""];
break;
}
}
NSArray *dateArray = [dateStr componentsSeparatedByString:@" "];
NSArray *hourArray = [[dateArray objectAtIndex:2] componentsSeparatedByString:@":"];
NSDateComponents *components = [[NSDateComponents alloc] init];
NSString *aux = [dateArray objectAtIndex:0];
int month = 0;
for (NSString *m in calendarMonths) {
month++;
if ([m isEqualToString:aux]) {
break;
}
}
components.month = month;
components.day = [[dateArray objectAtIndex:1] intValue];
components.hour = [[hourArray objectAtIndex:0] intValue];
components.minute = [[hourArray objectAtIndex:1] intValue];
components.second = [[hourArray objectAtIndex:2] intValue];
components.year = [[dateArray objectAtIndex:4] intValue];
NSTimeZone *gmt = [NSTimeZone timeZoneForSecondsFromGMT:2];
[components setTimeZone:gmt];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
[calendar setTimeZone:[NSTimeZone systemTimeZone]];
NSDate *date = [calendar dateFromComponents:components];
NSString *tweetDate = [self getTimeAsString:date];
NSString *tweetValues = [NSString stringWithFormat:@"%@ :%@",userName,tweetDate];
cell.textLabel.text = [NSString stringWithFormat:@"%@",tweetValues];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@",tweetContaint];
[cell.detailTextLabel setFont:[UIFont fontWithName:@"Helvetica" size:20]];
return cell;
}
- (NSString*)getTimeAsString:(NSDate *)lastDate {
NSTimeInterval dateDiff = [[NSDate date] timeIntervalSinceDate:lastDate];
int nrSeconds = dateDiff;//components.second;
int nrMinutes = nrSeconds / 60;
int nrHours = nrSeconds / 3600;
int nrDays = dateDiff / 86400; //components.day;
NSString *time;
if (nrDays > 5){
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateStyle:NSDateFormatterShortStyle];
[dateFormat setTimeStyle:NSDateFormatterNoStyle];
time = [NSString stringWithFormat:@"%@", [dateFormat stringFromDate:lastDate]];
} else {
// days=1-5
if (nrDays > 0) {
if (nrDays == 1) {
time = @"1 day ago";
} else {
time = [NSString stringWithFormat:@"%d days ago", nrDays];
}
} else {
if (nrHours == 0) {
if (nrMinutes < 2) {
time = @"just now";
} else {
time = [NSString stringWithFormat:@"%d minutes ago", nrMinutes];
}
} else { // days=0 hours!=0
if (nrHours == 1) {
time = @"1 hour ago";
} else {
time = [NSString stringWithFormat:@"%d hours ago", nrHours];
}
}
}
}
return [NSString stringWithFormat:NSLocalizedString(@"%@", @"label"), time];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 100;
}
The fundamental problem is that the standard imageView
property of the standard table view cell will automatically resize itself based upon the image that is present when cellForRowAtIndexPath
finishes. But since there is no image yet when you first present the table, the cell is laid out as if there's no image. And when you asynchronously update the image view's image
, it won't resize the image view.
There are a couple of ways of solving this:
Don't use the default imageView
provided by UITableViewCell
, but rather define your own custom cell subclass with an IBOutlet
to its own UIImageView
property. Make sure that this UIImageView
has a fixed layout (i.e., it doesn't use the intrinsic size derived from the underlying image).
If you do that, you can asynchronously update the image
property for your custom UIImageView
outlet, and because the layout was not contingent upon the presence of the image, any asynchronous updates of that image
should appear correctly.
When you receive the image, don't just set the image view's image
property, but rather reload the whole row associated with that NSIndexPath
using reloadRowsAtIndexPaths
.
If you do this, the cell will be laid out correctly assuming that you retrieve the image from the cache correctly, and do so before cellForRowAtIndexPath
finishes.
Note, if you do this, you will need to fix your getImageFromUrl
to actually try to retrieve the image from the cache first (and do this from the main queue, before to dispatch to the background queue), or else you'll end up in an endless loop.
Having said that, there are deeper problems here.
As I mentioned above, you're caching your images, but never using the cache when retrieving the images.
You are asynchronously updating the image view.
You should initialize the image
property of the UIImageView
before you initiate the new asynchronous fetch, otherwise when a cell is reused, you'll see the old image there until the new image is retrieved.
What if the cell was reused in the intervening period between calling getImageFromUrl
and when the asynchronous request finishes? You'll be updating the image view for the wrong cell. (This problem will be more apparent when doing this over a slow connection. Run your code using the network link conditioner to simulate slow connections and you'll see the problem I'm describing.)
What if the user rapidly scrolls down to the 100th row in the table? The network requests for the visible cells will be backlogged behind the other 99 image requests. You could even get timeout errors on slow connections.
There are a bunch of tactical little issues in getImageFromUrl
.
Why dispatching synchronously from global queue to another global queue? That's unnecessary. Why dispatching UI update synchronously to main thread? That's inefficient.
Why define imageData
as __block
outside of the block; just define it within the block and you don't need __block
qualifier.
What if you didn't receive a valid UIImage
from the network request (e.g. you got a 404 error message); the existing code would crash. There are all sorts of responses the server might provide which are not a valid image, and you really must identify that situation (i.e. make sure that not only was NSData
you received not nil
, but also that the UIImage
that you created from it was not nil
, too).
I'd probably use NSCache
rather than NSMutableDictionary
for the cache. Also, regardless of whether you use NSCache
or NSMutableDictionary
, you want to make sure that you respond to memory pressure events and empty that cache if needed.
We can go through all of these individual problems, but it's a non-trivial amount of work to fix all of this. I might therefore suggest you consider the UIImageView
categories of SDWebImage or AFNetworking. They take care of most of these issues, plus others. It will make your life much, much easier.