1

So I'm adding an Image to my SwiftUI list by getting(fetching) the path to the image and then using the image path to fetch the image from that url/path.

Grabbing the data/path

class Webservice {
    func getAllPosts(completion: @escaping ([Post]) -> ()) {
        guard let url = URL(string: "http://localhost:8000/albums")
     else {
     fatalError("URL is not correct!")
    }

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

            let posts = try!

                JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
                    completion(posts)
            }
        }.resume()
    }
}

Variables

struct Post: Codable, Hashable, Identifiable {

    let id: String
    let title: String
    let path: String
    let description: String
}

Setting the variables in Post to the posts in completion(posts) in class Webservice.

final class PostListViewModel: ObservableObject {

    init() {
        fetchPosts()
    }

    @Published var posts = [Post]()

    private func fetchPosts() {
        Webservice().getAllPosts {
            self.posts = $0
        }
    }


}

Fetch image from url/path

class ImageLoader: ObservableObject {
    var didChange = PassthroughSubject<Data, Never>()
    var data = Data() {
        didSet {
            didChange.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()
    }
}

Set @State image

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

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

    var body: some View {

            Image(uiImage: image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:100, height:100)
                .onReceive(imageLoader.didChange) { data in
                self.image = UIImage(data: data) ?? UIImage()
        }
    }
}

Then display my image in a list using ImageView with the url/path from the data I fetched in the beginning which gets passed into ImageLoader to grab the image and set @State image in ImageView

struct ContentView: View {
   init() {
            Webservice().getAllPosts {
                print($0)
            }
        }

    @ObservedObject var model = PostListViewModel()

        var body: some View {

                List(model.posts) { post in
                 VStack {
                    Text(post.title)
                    ImageView(withURL: "http://localhost:8000/\(post.path)")
                    Text(post.description)


                }
            }

        }

}

This code works to get the image at first but once I start scrolling here and there the images start to disappear. Why is this happening and how do I fix it?

Edit: I was told to cache the images to prevent loading again. Does anyone know how I would do that?

1 Answer 1

3

disappearing the image is because of reusing cells in the list and every time the cell reused it to fetch the image again, you need to cache the images to prevent it.

you can change your ImageLoader in this way and have a cache you need to change the image in your ImageView too, instead of Data use UIImage

class ImageLoader: 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
        }
    }
}

Cache class helper

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
    }
}

ImageView struct:


struct ImageView: View {
    @ObservedObject var imageLoader:ImageLoader

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

    var body: some View {

            Image(uiImage: imageLoader.image ?? UIImage() )
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:100, height:100)
    }
}
Sign up to request clarification or add additional context in comments.

13 Comments

What do I replace in this line?: .onReceive(imageLoader.didChange) { data in self.image = UIImage(data: data) ?? UIImage()
.onReceive(imageLoader.didChange) { data in self.image = data}
Hmmm. I used your code and my images still disappear
try to put self.imageCache.setObject(image, forKey: urlString as NSString) in Dispatch and check it
I put it in Dispatch and it didn't do the trick. What would you like me to check?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.