Search code examples
iosswiftgrand-central-dispatchuicollectionviewcell

Reload UICollectionViewCell which has image


I am trying to create something like Facebook NewsFeed where, I am using custom UICollectionViewCell to display data (Text/Image) from JSON. I have 2 different APIs. One for text and another for images(Every cell doesn't have image).

So, First of all I am getting all text values in to my cells from my textAPI and reloading myCollectionView. That works perfect.

Now for the Images, I am using ImageFetcher to fetch images,

func ImageFetcher(postId : NSNumber, completion : ((_ image: UIImage?) -> Void)!) {

    var image = UIImage()

    let urlString = "http://myImageAPI/Image/\(postId)"
    let jsonUrlString = URL(string: urlString)
    print(urlString)
    URLSession.shared.dataTask(with: jsonUrlString!) { (data, response, error) in

        do {
            if let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any] {
                if let images = jsonData["Image"] as? String {
                    if images == "" {
                        print("No Image")
                    } else {
                        let dataDecoded : Data? = Data(base64Encoded: images, options: .ignoreUnknownCharacters)
                        image = UIImage(data: dataDecoded!)!
                        completion(image)
                    }
                }
            }
            else {
                completion(nil)
            }
        } catch {
            print(error.localizedDescription)
        }
    }.resume()

}

To display image in to Cell,

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

// Displaying other elements with text

DispatchQueue.main.async { 
        self.ImageFetcher(postId: self.myArray[indexPath.item].id!, completion: { (image) -> Void in
            customCell.mainImage.image = image
        })
// Declared "indexPaths" var indexPaths = [IndexPath]()
// Added this lines
      // let indexPath = IndexPath(item: indexPath.item, section: 0)
      // self.indexPaths.append(indexPath) 
    }

return customCell
}


override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    self.jsonParsing()

}
func jsonParsing() {
    //Fetching text data from textAPI

    //DispatchQueue.main.async {
    //      self.collectionView.reloadItems(at: self.indexPaths)
    //}
}

Code works without any error but the issue is Images only appears when I click on them. (I can see empty/white imageView in a cell until I click it. As soon as I click on imageView the Image appears) I feel its something with DispatchQueue.main.async but, not sure.

I do not want to reload the whole collectionView after fetching the images. Just want to reload those cells which have Image. I found collectionView.reloadItemsAtIndexPaths(myArrayOfIndexPaths) on many solution but don't know how to make it work in this scenario. Can anyone please help me here? Any help will be much appreciated.


Solution

  • Can you try this?

    DispatchQueue.main.async {
          customCell.mainImage.image = image
     }
    

    instead of

    customCell.mainImage.image = image
    

    ===================== Updated ======================

    I ended up helping Snehal not only image load problem but also a couple of other issues with collection view and image caches. I suggested him to use a third party library like SDWebImage but found out his api returns image as base64 string in the response. So I just go ahead and clean up his code and write the code that I think it's a good string point that might help him.

    import UIKit 
    
    class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 
    
        let imageView = UIImageView() 
        lazy var collectionView: UICollectionView = { 
            let layout = UICollectionViewFlowLayout() 
            layout.minimumInteritemSpacing = 10 
            layout.minimumLineSpacing = 10 
            layout.scrollDirection = .vertical 
    
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 
            collectionView.delegate = self 
            collectionView.dataSource = self 
            collectionView.register(CustomCell.self, forCellWithReuseIdentifier: NSStringFromClass(CustomCell.self)) 
            collectionView.backgroundColor = .clear 
            return collectionView 
        }() 
    
    
        override func viewDidLoad() { 
            super.viewDidLoad() 
            view.addSubview(collectionView) 
        } 
    
        override func viewWillLayoutSubviews() { 
             super.viewWillLayoutSubviews() 
             collectionView.frame = view.bounds 
        } 
    
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 
            return 50 
        } 
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(CustomCell.self), for: indexPath) as! CustomCell 
            cell.imageUrl = "http://myImageAPI/Image/\(postId)" 
            return cell 
        } 
    
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 
             return CGSize(width: collectionView.frame.size.width - 2 * 20, height: 100) 
        } 
    
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 
    
    } 
    } 
    
    
    class CustomCell: UICollectionViewCell { 
    
        let imageView = UIImageView() 
        var imageUrl: String? { 
            didSet { 
                if let imageUrl = imageUrl, let url = URL(string: imageUrl) { 
                    dataTask = imageView.loadImage(url: url) 
                } 
    
            } 
        } 
    
        var dataTask: URLSessionDataTask? 
    
        override init(frame: CGRect) { 
            super.init(frame: frame) 
    
            backgroundColor = .white 
            imageView.backgroundColor = UIColor.lightGray 
            imageView.contentMode = .scaleAspectFit 
            contentView.addSubview(imageView) 
        } 
    
        required init?(coder aDecoder: NSCoder) { 
            fatalError("init(coder:) has not been implemented") 
        } 
    
        override func prepareForReuse() { 
            super.prepareForReuse() 
            dataTask?.cancel() 
            imageView.image = nil 
        } 
    
        override func layoutSubviews() { 
            super.layoutSubviews() 
            imageView.frame = bounds 
        } 
    } 
    
    
    extension UIImageView { 
        @discardableResult func loadImage(url: URL) -> URLSessionDataTask? { 
            if let image = ImageLoadManager.manager.cachedImages[url.absoluteString] { 
                self.image = image 
                return nil 
            } 
            let task = URLSession.shared.dataTask(with: url) { (data, response, error) in 
                do { 
                    guard let data = data else { 
                        return 
                    } 
                    if let jsonData = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any] { 
                    if let images = jsonData["PostImage"] as? String {// Pase the Base64 image string
                         if images == "" { 
                              print("No Image") 
                         } else { 
                             if let dataDecoded = Data(base64Encoded: images, options: .ignoreUnknownCharacters), let decodedImage = UIImage(data: dataDecoded) { 
                                 DispatchQueue.main.async { 
                       ImageLoadManager.manager.cachedImages[url.absoluteString] = decodedImage 
                       self.image = decodedImage 
                                  } 
                             } 
                         } 
                     } 
                 } 
                 else { 
                      DispatchQueue.main.async { 
                          self.image = nil 
                      } 
                 } 
                } catch { 
                     print(error.localizedDescription) 
                } 
            } 
            task.resume() 
            return task 
        } 
    } 
    
    class ImageLoadManager { 
        static let manager = ImageLoadManager() 
        var cachedImages = [String: UIImage]() 
    }