0
\$\begingroup\$

In my SwiftUI app, I'm trying to get some data from a json Api and display this data along with an image. First I have a function that fetches the data from the Api, then I have another function that downloads the image data. The image takes a while to appear(1-2) seconds. I'm wondering if this is the best way to do it, especially if I'm downloading multiple images to display them in a List or a Grid.

func fetchAPI() {

    URLSession.shared.dataTask(with: url) { (data, response, error) in

/* Code that decodes the json and gets the imageURL string */

            DispatchQueue.main.async {

                self.downloadImage(url: imageURL) // call the function that downloads the imageData
            }
        } catch {
            print("Error: \(error.localizedDescription)")
        }
    }
    .resume()
} 

The function that downloads the image:

func downloadImage(url: String) {

    guard let imageURL = URL(string: url) else {
        print("Error: Invalid image URL")
        return
    }

    URLSession.shared.dataTask(with: imageURL) { data, response, error in
        guard let recievedData = data, error == nil  else {
            print("Error: No Data")
            return
        }

        DispatchQueue.main.async {
            self.imageData = recievedData 
        }
    }
    .resume()
}
\$\endgroup\$
0
\$\begingroup\$

Maybe the image is in question is a little bit large or the internet connection used is a little bit slow.

I personally would go about it differently.

For UIKit

import UIKit
import Foundation

extension UIImageView {
    func load(url: URL) {
        DispatchQueue.global().async { [weak self] in
            if let data = try? Data(contentsOf: url) {
                if let image = UIImage(data: data) {
                    DispatchQueue.main.async {
                        self?.image = image
                    }
                }
            }
        }
    }
}

For SwiftUI we have to go about it a little bit differently but there are two ways

The very simple one-time usage:

import Foundation
import SwiftUI
import Combine

// Image Loader
class ImageLoader: ObservableObject { 
    var dataPublisher = PassthroughSubject<Data, Never>() 
    var data = Data() { 
        didSet { 
            dataPublisher.send(data) 
        } 
     }

     init(urlString:String) { 
        guard let url = URL(string: urlString) else { return } 
        let task = URLSession.shared.dataTask(with: url) { data, response, error in 
            guard let data = data else { return }         
            DispatchQueue.main.async { 
                self.data = data 
            } 
        } 
        task.resume() 
     }
}

// Image View
struct ImageView: View {
    @ObservedObject var imageLoader:ImageLoader
    @State var image:UIImage = UIImage()

    init(withURL url:String) {
        imageLoader = ImageLoader(urlString: url)
    }

    var body: some View {
        VStack { 
            Image(uiImage: image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:100, height:100)
        }.onReceive(imageLoader.dataPublisher) { data in
            self.image = UIImage(data: data) ?? UIImage()
        }
    }
}

struct ImageView_Previews: PreviewProvider {
    static var previews: some View {
        ImageView(withURL: "")
    }
}

The more advanced way

import Foundation
import SwiftUI
import Combine

// ImageCache
protocol ImageCache {
    subscript(_ url: URL) -> UIImage? { get set }
}

struct TemporaryImageCache: ImageCache {
    private let cache: NSCache<NSURL, UIImage> = {
        let cache = NSCache<NSURL, UIImage>()
        cache.countLimit = 100 // 100 items
        cache.totalCostLimit = 1024 * 1024 * 100 // 100 MB
        return cache
    }()
    
    subscript(_ key: URL) -> UIImage? {
        get {
            self.cache.object(forKey: key as NSURL)
        }
        set {
            newValue == nil ? self.cache.removeObject(forKey: key as NSURL) : self.cache.setObject(newValue!, forKey: key as NSURL)
        }
    }
}

// EnvironmentValues+ImageCache
struct ImageCacheKey: EnvironmentKey {
    static let defaultValue: ImageCache = TemporaryImageCache()
}

extension EnvironmentValues {
    var imageCache: ImageCache {
        get {
            self[ImageCacheKey.self]
        }
        set {
            self[ImageCacheKey.self] = newValue
        }
    }
}

// Image Loader
class ImageLoader: ObservableObject {
    @Published
    var image: UIImage?
    
    private(set) var isLoading = false
    
    private let url: URL
    private var cache: ImageCache?
    private var cancellable: AnyCancellable?
    
    private static let imageDownloadingQueue = DispatchQueue(label: "image-downloading")
    
    init(url: URL, cache: ImageCache? = nil) {
        self.url = url
        self.cache = cache
    }
    
    deinit {
        self.cancel()
    }
    
    func load() {
        guard !self.isLoading else { return }
        
        if let image = self.cache?[self.url] {
            self.image = image
            return
        }
        
        self.cancellable = URLSession.shared.dataTaskPublisher(for: self.url)
            .map {
                UIImage(data: $0.data)
            }
            .replaceError(with: nil)
            .handleEvents(receiveSubscription: { [weak self] _ in
                self?.onStart()
            },
            receiveOutput: { [weak self] in
                self?.cache($0)
            },
            receiveCompletion: { [weak self] _ in
                self?.onFinish()
            },
            receiveCancel: { [weak self] in
                self?.onFinish()
            })
            .subscribe(on: Self.imageDownloadingQueue)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.image = $0
            }
    }
    
    func cancel() {
        self.cancellable?.cancel()
    }
    
    private func onStart() {
        self.isLoading = true
    }
    
    private func onFinish() {
        self.isLoading = false
    }
    
    private func cache(_ image: UIImage?) {
        image.map {
            self.cache?[self.url] = $0
        }
    }
}

// Image View
struct ImageView<Placeholder: View>: View {
    @StateObject
    private var loader: ImageLoader
    private let placeholder: Placeholder
    private let image: (UIImage) -> Image
    
    init(url: URL, @ViewBuilder placeholder: () -> Placeholder, @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)) {
        self.placeholder = placeholder()
        self.image = image
        self._loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
    }
        
    var body: some View {
        self.content.onAppear(perform: loader.load)
    }
    
    private var content: some View {
        Group {
            if let image = self.loader.image {
                self.image(image)
            } else {
                self.placeholder
            }
        }
    }
}

```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.