1

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.

Ollie
  • 1,926
  • 1
  • 20
  • 35
iUser
  • 1,075
  • 3
  • 20
  • 49
  • [Take ref from here.](https://stackoverflow.com/questions/28206492/refresh-certain-row-of-uitableview-based-on-int-in-swift) If you still need help you can ask. – dahiya_boy Jul 27 '17 at 16:31
  • Thanks!! Let me try it. – iUser Jul 27 '17 at 16:36
  • Can you little bit more explain this -> **the issue is Images only appears when I click on them** – dahiya_boy Jul 27 '17 at 16:38
  • Updated the question. please let me know if you want any other detail. – iUser Jul 27 '17 at 16:49
  • please add full code of textMethods ,`cellForItem` & `didSelectRow`. your code seems to be perfect. you dont need to reload. so isuue is somewhere else. – dahiya_boy Jul 27 '17 at 16:55
  • It was because of the dispatchqueue as "HMHero" suggested below. Was trying many things since an hour. Thank you for the help @dahiya_boy – iUser Jul 27 '17 at 17:06

2 Answers2

3

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]() 
}
HMHero
  • 2,333
  • 19
  • 11
  • Omg!! :| this was the issue. Ahh.. Thank you buddy for your help!! – iUser Jul 27 '17 at 17:03
  • Hey HMHero, I am getting same issue again. I have updated my code. I am using cached image to display this time. This time images doesn't display until I scroll the collectionView. Once I scroll all the images are there on right place. – iUser Aug 02 '17 at 15:09
  • 1
    @Snehal it's probably the same issue. The idea is that you have to make sure where ever you set the image to image view, it has to be in the main thread. – HMHero Aug 02 '17 at 16:06
  • It is in the main thread. [Here](https://stackoverflow.com/q/45465737/755621) is the updated code. – iUser Aug 02 '17 at 16:10
  • What about when you are setting the image in the completion block? First of all move the DispatchQueue.main.async to inside the ImageFetcher. change completion(image) to DispatchQueue.main.async { completion(image) } and completion(nil) to DispatchQueue.main.async { completion(nil) }. Remove the DispatchQueue.main.async from setting image. – HMHero Aug 02 '17 at 16:18
  • Why don't you use third party library like https://github.com/rs/SDWebImage? It handles image load and cache and more nicely. – HMHero Aug 02 '17 at 16:21
  • Yes, I am trying to cache Image too. – iUser Aug 02 '17 at 16:22
  • I am converting base64 string to image. Will it work with this third party library ? I didn't look in to it. Let me check !! Thanks again. – iUser Aug 02 '17 at 16:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/150850/discussion-between-hmhero-and-snehal). – HMHero Aug 02 '17 at 16:26
  • Hey HMHero, I am having same problem when fetching **ImageArray** instead of single image like last time. Can you please give any suggestion to https://stackoverflow.com/q/45964418/755621 . – iUser Aug 30 '17 at 15:53
0

Better way to do this is like make a model that class that contains url of images and your text variables like class AnyName { var url: String? var desc: String?

init(urlValue: String?,  descValue:String? ){
    url = urlValue
    desc = descValue
}

}

Now in your viewcontroller make array of above class name var items = AnyName Now by first api hitting you can set values for text in this array and passed to collectionview and load cells with text and thereafter you can change arrayvalues for url and reload collectionview easily

Sand'sHell811
  • 358
  • 3
  • 15