Je développe une application iOS 4 avec le SDK iOS 5.0 et XCode 4.2.
Je dois montrer quelques articles de blog dans un UITableView. Lorsque j'ai récupéré toutes les données du service Web, j'utilise cette méthode pour créer un UITableViewCell:
- (BlogTableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* cellIdentifier = @"BlogCell";
BlogTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSArray* topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BlogTableViewCell" owner:nil options:nil];
for(id currentObject in topLevelObjects)
{
if ([currentObject isKindOfClass:[BlogTableViewCell class]])
{
cell = (BlogTableViewCell *)currentObject;
break;
}
}
}
BlogEntry* entry = [blogEntries objectAtIndex:indexPath.row];
cell.title.text = entry.title;
cell.text.text = entry.text;
cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];
return cell;
}
Mais cette ligne:
cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];
c'est tellement lent (entry.photo a une URL http).
Existe-t-il un moyen de charger cette image de manière asynchrone? Je pense que c'est difficile car tableView: cellForRowAtIndexPath est appelé très souvent.
Jetez un œil à SDWebImage:
https://github.com/rs/SDWebImage
C'est un ensemble fantastique de classes qui s'occupent de tout pour vous.
Tim
J'ai écrit une classe personnalisée pour ce faire, en utilisant des blocs et GCD:
WebImageOperations.h
#import <Foundation/Foundation.h>
@interface WebImageOperations : NSObject {
}
// This takes in a string and imagedata object and returns imagedata processed on a background thread
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end
WebImageOperations.m
#import "WebImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation WebImageOperations
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
NSURL *url = [NSURL URLWithString:urlString];
dispatch_queue_t callerQueue = dispatch_get_current_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
dispatch_async(downloadQueue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(callerQueue, ^{
processImage(imageData);
});
});
dispatch_release(downloadQueue);
}
@end
Et dans votre ViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Pass along the URL to the image (or change it if you are loading there locally)
[WebImageOperations processImageDataWithURLString:entry.photo andBlock:^(NSData *imageData) {
if (self.view.window) {
UIImage *image = [UIImage imageWithData:imageData];
cell.photo.image = image;
}
}];
}
Il est très rapide et chargera les images sans affecter l'interface utilisateur ou la vitesse de défilement de TableView.
*** Remarque - Cet exemple suppose que l'ARC est utilisé. Sinon, vous devrez gérer vos propres versions sur les objets)
Dans iOS 6 et versions ultérieures, dispatch_get_current_queue donne des avertissements de dépréciation.
Voici une alternative qui est une synthèse de la réponse @ElJay ci-dessus et de l'article de @khanlou ici .
Créez une catégorie sur UIImage:
UIImage + Helpers.h
@interface UIImage (Helpers)
+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback;
@end
UIImage + Helpers.m
#import "UIImage+Helpers.h"
@implementation UIImage (Helpers)
+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:imageData];
callback(image);
});
});
}
@end
Rapide:
extension UIImage {
// Loads image asynchronously
class func loadFromURL(url: NSURL, callback: (UIImage)->()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
let imageData = NSData(contentsOfURL: url)
if let data = imageData {
dispatch_async(dispatch_get_main_queue(), {
if let image = UIImage(data: data) {
callback(image)
}
})
}
})
}
}
Usage:
// First of all remove the old image (required for images in cells)
imageView.image = nil
// Load image and apply to the view
UIImage.loadFromURL("http://...", callback: { (image: UIImage) -> () in
self.imageView.image = image
})
Considérant le cas d'échec.
- (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSError * error = nil;
NSData * imageData = [NSData dataWithContentsOfURL:url options:0 error:&error];
if (error)
callback(nil);
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:imageData];
callback(image);
});
});
}
Swift 4 | Chargement asynchrone de l'image
Créez une nouvelle classe nommée ImageLoader.Swift
import UIKit
class ImageLoader {
var cache = NSCache<AnyObject, AnyObject>()
class var sharedInstance : ImageLoader {
struct Static {
static let instance : ImageLoader = ImageLoader()
}
return Static.instance
}
func imageForUrl(urlString: String, completionHandler:@escaping (_ image: UIImage?, _ url: String) -> ()) {
let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData
if let imageData = data {
let image = UIImage(data: imageData as Data)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
return
}
let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
if error == nil {
if data != nil {
let image = UIImage.init(data: data!)
self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
DispatchQueue.main.async {
completionHandler(image, urlString)
}
}
} else {
completionHandler(nil, urlString)
}
}
downloadTask.resume()
}
}
Pour utiliser dans votre classe ViewController:
ImageLoader.sharedInstance.imageForUrl(urlString: "https://www.logodesignlove.com/images/classic/Apple-logo-rob-janoff-01.jpg", completionHandler: { (image, url) in
if image != nil {
self.imageView.image = image
}
})
Oui, c'est relativement facile. L'idée est quelque chose comme:
[self.tableView reloadData]
J'ai testé cette approche à plusieurs reprises et donne d'excellents résultats. Si vous avez besoin d'aide/d'exemples avec la partie asynchrone (je recommande d'utiliser gcd pour cela) faites le moi savoir.
vous pouvez facilement le faire parfaitement si vous utilisez l'exemple de code fourni par Apple à cet effet: l'exemple de code: Lazy Image
Regardez simplement le délégué rowforcell et ajoutez des fichiers icondownloader à votre projet.
Le seul changement que vous devez faire est de changer un objet d'enregistrement avec votre objet.
Bien que SDWebImage et d'autres solutions tierces soient peut-être d'excellentes solutions, si vous ne souhaitez pas utiliser des API tierces, vous pouvez également développer votre propre solution.
Reportez-vous à ce tutoriel sur le chargement paresseux qui explique également comment modéliser vos données dans la vue tableau.
Vous devrez probablement sous-classer votre UIImageView. J'ai récemment fait un projet simple pour expliquer cette tâche particulière - chargement d'image asynchrone en arrière-plan - jetez un œil à mon projet sur GitHub. Plus précisément, regardez la classe KDImageView .