4FunAndProfit 4FunAndProfit - 6 months ago 572
iOS Question

AFNetworking 3 Async image download queue

I'm using AFNetworking for all my communication with my server, everything is put in a separate class called 'APIConnector'.

I'm trying to download a User. When i have the user id i can generate a URL but i have no idea which users i get from the server before i receive them. The code i have right now looks like this:

AFHTTPSessionManager *httpManager = [AFHTTPSessionManager manager];
AFHTTPSessionManager *imageManager = [AFHTTPSessionManager manager]; //Used for posting to the server
[httpManager POST:url parameters:dict progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
UIImageView *imageHolder = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"placeholder"]];
if (isUser) {
NSMutableArray *users = [NSMutableArray new];
int i = 1;
for (id userResult in responseObject) {
User *user = [[User alloc] initWithId:[userResult valueForKey:@"index"] name:[userResult valueForKey:@"name"]];
user.imageURL = [NSString stringWithFormat:@"%@/image/?index=%@&imgIndex=0",backendURL,user.userID];
[users addObject:user];
if (i == [responseObject count]) {
success(users);
}
i++;
}


I've done some iterations changing a few things, having tried it like this and also like this but with operations:

AFHTTPSessionManager *imageManager = [AFHTTPSessionManager manager]; //Used for posting to the server
[httpManager POST:url parameters:dict progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
UIImageView *imageHolder = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"placeholder"]];
if (isUser) {
NSMutableArray *users = [NSMutableArray new];
int i = 1;
for (id userResult in responseObject) {
User *user = [[User alloc] initWithId:[userResult valueForKey:@"index"] name:[userResult valueForKey:@"name"]];
user.imageURL = [NSString stringWithFormat:@"url to image"];
[users addObject:user];
if (i == [responseObject count]) {
success(users);
}
i++;
}
NSMutableArray *usersWithImage = [NSMutableArray new];
for (int i = 0; i < users.count;i++) {
User *userImg = [users objectAtIndex:i];
NSString *imageURL = [NSString stringWithFormat:@"%@/image/?index=%@&imgIndex=0",backendURL,userImg.userID];
imageManager.responseSerializer = [AFImageResponseSerializer serializer];
[imageManager GET:imageURL parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable imgResponse) {
User *user = userImg;[users objectAtIndex:i];
user.profilePic.image = imgResponse;
[usersWithImage addObject:user];

if (i == users.count-1) {
success(usersWithImage);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Error: %@",error);
}];


It ''works'' but it just loads the last image in as all the image, i've also tried: https://github.com/robertmryan/AFHTTPSessionOperation/ same result

EDIT/ADD:

NSMutableArray *users = [NSMutableArray new];
dispatch_group_t group = dispatch_group_create();
for (id acResult in responseObject) {
User *user = [[User alloc]initWithId:[acResult valueForKey:@"index"] name:[acResult valueForKey:@"name"]];
NSString *imageURL = [NSString stringWithFormat:@"%@/image/?index=%@&imgIndex=0",backendURL,user.userID];
dispatch_group_enter(group);
[imageManager GET:imageURL parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
user.imageData = UIImageJPEGRepresentation(responseObject, 1);
[users addObject:user];
dispatch_group_leave(group); // leave if successful
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Error: %@",error);
dispatch_group_leave(group); // leave if not
}];
}


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
success(activities); //This is never called
});
Also i have control over how many users arrive (only 10 per call) but i have no idea which id they have.

Rob Rob
Answer

In your second example, you have:

if (i == users.count-1) {
    success(usersWithImage);
}

You should either use dispatch group or NSOperation wrapper to determine when all the downloads are done, e.g.:

imageManager.responseSerializer = [AFImageResponseSerializer serializer]; // you only have to do this once

NSMutableArray *usersWithImage = [NSMutableArray new];

dispatch_group_t group = dispatch_group_create();

for (User *user in users) {
    NSString *imageURL = [NSString stringWithFormat:@"%@/image/?index=%@&imgIndex=0", backendURL, user.userID];
    dispatch_group_enter(group);
    [imageManager GET:imageURL parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id imgResponse) {
        user.profilePic.image = imgResponse;   // this line is worrying, though: What are you doing here
        [usersWithImage addObject:user];
        dispatch_group_leave(group);           // leave if successful
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"Error: %@",error);
        dispatch_group_leave(group);           // leave if not
    }];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    success(usersWithImage);
});

I must confess, though, that I find the reference to user.profilePic.image a little worrying. Is that a UIKit reference? You shouldn't have UIKit controls in a model object.


As a broader issue, if you don't know how many users you might be retrieving, downloading all of the images may not be a wise use of memory and network resources. What if there were 100 users? Downloading the list of 100 users will be quick, but downloading 100 images won't. Generally you should just save the image URL in your model, but don't request the images until the UI needs them (and only download the ones needed, e.g. if only 12 rows visible, only download those 12 images). If you search for "[ios] lazy loading images", you'll probably find many discussions on this topic.

Personally, given that you're already using AFNetworking, I'd be inclined to use the UIImageView+AFNetworking category (found in the UIKit+AFNetworking folder). This is a really graceful way to do lazy asynchronous image retrieval for a UIImageView in a UITableViewCell.

Comments