2

Learning how to cache image with native NSCache in YouTube.

Below is the example code, which is working well. I could see it set the image into cache in getImageFromResponse method after data is returned from URLSession.shared.dataTask. Well, it is the old way that using the @escaping completion handler.

import Foundation
import SwiftUI

class UrlImageModel: ObservableObject {
    @Published var image: UIImage?
    var urlString: String?
    var imageCache = ImageCache.getImageCache()

    init(urlString: String?) {
        self.urlString = urlString
        loadImage()
    }

    func loadImage() {
        if loadImageFromCache() {
            print("Cache hit")
            return
        }

        print("Cache miss, loading from url")
        loadImageFromUrl()
    }

    func loadImageFromCache() -> Bool {
        guard let urlString = urlString else {
            return false
        }

        guard let cacheImage = imageCache.get(forKey: urlString) else {
            return false
        }

        image = cacheImage
        return true
    }

    func loadImageFromUrl() {
        guard let urlString = urlString else {
            return
        }

        let url = URL(string: urlString)!
        let task = URLSession.shared.dataTask(with: url, completionHandler: getImageFromResponse(data:response:error:))
        task.resume()
    }


    func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) {
        guard error == nil else {
            print("Error: \(error!)")
            return
        }
        guard let data = data else {
            print("No data found")
            return
        }

        DispatchQueue.main.async {
            guard let loadedImage = UIImage(data: data) else {
                return
            }

            self.imageCache.set(forKey: self.urlString!, image: loadedImage)
            self.image = loadedImage
        }
    }
}

class ImageCache {
    var cache = NSCache<NSString, UIImage>()

    func get(forKey: String) -> UIImage? {
        return cache.object(forKey: NSString(string: forKey))
    }

    func set(forKey: String, image: UIImage) {
        cache.setObject(image, forKey: NSString(string: forKey))
    }
}

extension ImageCache {
    private static var imageCache = ImageCache()
    static func getImageCache() -> ImageCache {
        return imageCache
    }
}

And my app is using the latest Combine framework to download image. I try to add a line in loadImageFromUrl(), I think after assign(to: \.image, on: self), the image should has a value. However, the console gave me the Error "Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/Chuck/Documents/iOS/Projects/Moments/Moments/Model/Url Image/UrlImageModel.swift, line 55".

So by using native NSCache, how or where to set the image cache when with Combine networking? Below is my code, please give me some hint!

import Foundation
import SwiftUI
import Combine

class UrlImageModel: ObservableObject {
    @Published var image: UIImage?
    private var url: URL?
    private var cancellable: AnyCancellable?
    private var imageCache = ImageCache.getImageCache()

    init(url: URL?) {
        self.url = url
        loadImage()
    }

    func loadImage() {
        if loadImageFromCache() {
            print("Cache hit")
            return
        }

        print("Cache missing, loading from url")
        loadImageFromUrl()
    }

    func loadImageFromCache() -> Bool {
        guard let url = url else {
            return false
        }

        guard let cacheImage = imageCache.get(key: url) else {
            return false
        }

        image = cacheImage
        return true
    }

    func loadImageFromUrl() {
        cancellable = URLSession.shared.dataTaskPublisher(for: url!)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            .receive(on: DispatchQueue.main)
            .assign(to: \.image, on: self)

        // set image as cache!!
        imageCache.set(key: url!, image: image!)

    }

    func cancel() {
        cancellable?.cancel()
    }
}

class ImageCache {
    var cache = NSCache<NSURL, UIImage>()

    func get(key: URL) -> UIImage? {
        return cache.object(forKey: key as NSURL)
    }

    func set(key: URL, image: UIImage) {
        cache.setObject(image, forKey: key as NSURL)
    }
}

extension ImageCache {
    private static var imageCache = ImageCache()

    static func getImageCache() -> ImageCache {
        return imageCache
    }
}

/ / / Update, I found how to do it. Add Combine handleEventsmethod and set image cache in receiveOutput closure fix the issue. Btw, there is more parameters that dealing with upstream publisher & downstream receiver. For me, that needs more practice to consume this new way of networking in Combine.

Fixed Code, I also add a subscript in image cache part.

import Foundation
import SwiftUI
import Combine

class UrlImageModel: ObservableObject {
    @Published var image: UIImage?
    private var url: URL?
    private var cancellable: AnyCancellable?
    private var imageCache = ImageCache.getImageCache()

    init(url: URL?) {
        self.url = url
        loadImage()
    }

    func loadImage() {
        if loadImageFromCache() {
            print("Cache hit")
            return
        }

        print("Cache missing, loading from url")
        loadImageFromUrl()
    }

    func loadImageFromCache() -> Bool {
        guard let url = url else {
            return false
        }

        guard let cacheImage = imageCache[url] else {
            return false
        }

        image = cacheImage
        return true
    }

    func loadImageFromUrl() {
        guard let url = url else {
            return
        }

        cancellable = URLSession.shared.dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            // set image into cache!
            .handleEvents(receiveOutput: { [unowned self] image in
                guard let image = image else {return}
                self.imageCache[url] = image
            })
            .receive(on: DispatchQueue.main)
            .assign(to: \.image, on: self)
    }
}

class ImageCache {
    var cache = NSCache<NSURL, UIImage>()

    subscript(_ key: URL) -> UIImage? {
        get { cache.object(forKey: key as NSURL) }
        set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
    }
}

extension ImageCache {
    private static var imageCache = ImageCache()

    static func getImageCache() -> ImageCache {
        return imageCache
    }
}
Zhou Haibo
  • 1,681
  • 1
  • 12
  • 32

0 Answers0