2

I want to create a method to cache an image from an URL, I got the code in Swift since I had used it before, how can I do something similar to this in Objective-C:

import UIKit

let imageCache: NSCache = NSCache<AnyObject, AnyObject>()

extension UIImageView {
    
    func loadImageUsingCacheWithUrlString(urlString: String) {
        
        self.image = nil
        
        if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
            self.image = cachedImage
            return
        }
        
        let url = URL(string: urlString)
        if let data = try? Data(contentsOf: url!) {
            
            DispatchQueue.main.async(execute: {
                
                if let downloadedImage = UIImage(data: data) {
                    imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
                    
                    self.image = downloadedImage
                }
            })
        }
    }
    
}
Didami
  • 210
  • 2
  • 14

2 Answers2

2

Before you convert this, you might consider refactoring to make it asynchronous:

  1. One should never use Data(contentsOf:) for network requests because (a) it is synchronous and blocks the caller (which is a horrible UX, but also, in degenerate cases, can cause the watchdog process to kill your app); (b) if there is a problem, there’s no diagnostic information; and (c) it is not cancelable.

  2. Rather than updating image property when done, you should consider completion handler pattern, so caller knows when the request is done and the image is processed. This pattern avoids race conditions and lets you have concurrent image requests.

  3. When you use this asynchronous pattern, the URLSession runs its completion handlers on background queue. You should keep the processing of the image and updating of the cache on this background queue. Only the completion handler should be dispatched back to the main queue.

  4. I infer from your answer, that your intent was to use this code in a UIImageView extension. You really should put this code in a separate object (I created a ImageManager singleton) so that this cache is not only available to image views, but rather anywhere where you might need images. You might, for example, do some prefetching of images outside of the UIImageView. If this code is buried in the

Thus, perhaps something like:

final class ImageManager {
    static let shared = ImageManager()

    enum ImageFetchError: Error {
        case invalidURL
        case networkError(Data?, URLResponse?)
    }

    private let imageCache = NSCache<NSString, UIImage>()

    private init() { }

    @discardableResult
    func fetchImage(urlString: String, completion: @escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask? {
        if let cachedImage = imageCache.object(forKey: urlString as NSString) {
            completion(.success(cachedImage))
            return nil
        }

        guard let url = URL(string: urlString) else {
            completion(.failure(ImageFetchError.invalidURL))
            return nil
        }

        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                error == nil,
                let responseData = data,
                let httpUrlResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpUrlResponse.statusCode,
                let image = UIImage(data: responseData)
            else {
                DispatchQueue.main.async {
                    completion(.failure(error ?? ImageFetchError.networkError(data, response)))
                }
                return
            }

            self.imageCache.setObject(image, forKey: urlString as NSString)
            DispatchQueue.main.async {
                completion(.success(image))
            }
        }

        task.resume()

        return task
    }

}

And you'd call it like:

ImageManager.shared.fetchImage(urlString: someUrl) { result in
    switch result {
    case .failure(let error): print(error)
    case .success(let image): // do something with image
    }
}

// but do not try to use `image` here, as it has not been fetched yet

If you wanted to use this in a UIImageView extension, for example, you could save the URLSessionTask, so that you could cancel it if you requested another image before the prior one finished. (This is a very common scenario if using this in table views and the user scrolls very quickly, for example. You do not want to get backlogged in a ton of network requests.) We could

extension UIImageView {
    private static var taskKey = 0
    private static var urlKey = 0

    private var currentTask: URLSessionTask? {
        get { objc_getAssociatedObject(self, &Self.taskKey) as? URLSessionTask }
        set { objc_setAssociatedObject(self, &Self.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    private var currentURLString: String? {
        get { objc_getAssociatedObject(self, &Self.urlKey) as? String }
        set { objc_setAssociatedObject(self, &Self.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    func setImage(with urlString: String) {
        if let oldTask = currentTask {
            currentTask = nil
            oldTask.cancel()
        }

        image = nil

        currentURLString = urlString

        let task = ImageManager.shared.fetchImage(urlString: urlString) { result in
            // only reset if the current value is for this url

            if urlString == self.currentURLString {
                self.currentTask = nil
                self.currentURLString = nil
            }

            // now use the image

            if case .success(let image) = result {
                self.image = image
            }
        }

        currentTask = task
    }
}

There are tons of other things you might do in this UIImageView extension (e.g. placeholder images or the like), but by separating the UIImageView extension from the network layer, one keeps these different tasks in their own respective classes (in the spirit of the single responsibility principle).


OK, with that behind us, let us look at the Objective-C rendition. For example, you might create an ImageManager singleton:

//  ImageManager.h

@import UIKit;

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, ImageManagerError) {
    ImageManagerErrorInvalidURL,
    ImageManagerErrorNetworkError,
    ImageManagerErrorNotValidImage
};

@interface ImageManager : NSObject

// if you make this singleton, mark normal instantiation methods as unavailable ...

+ (instancetype)alloc __attribute__((unavailable("alloc not available, call sharedImageManager instead")));
- (instancetype)init __attribute__((unavailable("init not available, call sharedImageManager instead")));
+ (instancetype)new __attribute__((unavailable("new not available, call sharedImageManager instead")));
- (instancetype)copy __attribute__((unavailable("copy not available, call sharedImageManager instead")));

// ... and expose singleton access point

@property (class, nonnull, readonly, strong) ImageManager *sharedImageManager;

// provide fetch method

- (NSURLSessionTask * _Nullable)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completion;

@end

NS_ASSUME_NONNULL_END

and then implement this singleton:

//  ImageManager.m

#import "ImageManager.h"

@interface ImageManager()
@property (nonatomic, strong) NSCache<NSString *, UIImage *> *imageCache;
@end

@implementation ImageManager

+ (instancetype)sharedImageManager {
    static dispatch_once_t onceToken;
    static ImageManager *shared;
    dispatch_once(&onceToken, ^{
        shared = [[self alloc] initPrivate];
    });
    return shared;
}

- (instancetype)initPrivate
{
    self = [super init];
    if (self) {
        _imageCache = [[NSCache alloc] init];
    }
    return self;
}

- (NSURLSessionTask *)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage *image, NSError *error))completion {
    UIImage *cachedImage = [self.imageCache objectForKey:urlString];
    if (cachedImage) {
        completion(cachedImage, nil);
        return nil;
    }

    NSURL *url = [NSURL URLWithString:urlString];
    if (!url) {
        NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorInvalidURL userInfo:nil];
        completion(nil, error);
        return nil;
    }

    NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
            return;
        }

        if (!data) {
            NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNetworkError userInfo:nil];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
        }

        UIImage *image = [UIImage imageWithData:data];
        if (!image) {
            NSDictionary *userInfo = @{
                @"data": data,
                @"response": response ? response : [NSNull null]
            };
            NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNotValidImage userInfo:userInfo];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
        }

        [self.imageCache setObject:image forKey:urlString];
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(image, nil);
        });
    }];

    [task resume];

    return task;
}

@end

And you'd call it like:

[[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
    if (error) {
        NSLog(@"%@", error);
        return;
    }

    // do something with `image` here ...
}];

// but not here, because the above runs asynchronously

And, again, you could use this from within a UIImageView extension:

#import <objc/runtime.h>

@implementation UIImageView (Cache)

- (void)setImage:(NSString *)urlString
{
    NSURLSessionTask *oldTask = objc_getAssociatedObject(self, &taskKey);
    if (oldTask) {
        objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        [oldTask cancel];
    }

    image = nil

    objc_setAssociatedObject(self, &urlKey, urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    NSURLSessionTask *task = [[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
        NSString *currentURL = objc_getAssociatedObject(self, &urlKey);
        if ([currentURL isEqualToString:urlString]) {
            objc_setAssociatedObject(self, &urlKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }

        if (image) {
            self.image = image;
        }
    }];

    objc_setAssociatedObject(self, &taskKey, task, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
Rob
  • 415,655
  • 72
  • 787
  • 1,044
0

After trial and error this worked:

#import "UIImageView+Cache.h"

@implementation UIImageView (Cache)

NSCache* imageCache;

- (void)loadImageUsingCacheWithUrlString:(NSString*)urlString {
    
    imageCache = [[NSCache alloc] init];
    
    self.image = nil;
    
    UIImage *cachedImage = [imageCache objectForKey:(id)urlString];
    
    if (cachedImage != nil) {
        self.image = cachedImage;
        return;
    }
    
    NSURL *url = [NSURL URLWithString:urlString];
    
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    if (data != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            
            UIImage *downloadedImage = [UIImage imageWithData:data];
            
            if (downloadedImage != nil) {
                [imageCache setObject:downloadedImage forKey:urlString];
                self.image = downloadedImage;
            }
        });
    }
}

@end
Didami
  • 210
  • 2
  • 14