TableView with labels, images, gifs and video hangs / gets stuck incorrect while fetch from firestore in iOS, Swift

Viewed 1068

I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.

My model is like,

struct PostiisCollection {
    var id :String?
    var userID: String?
    var leadDetails : NSDictionary?
    var company: NSDictionary?
    var content: String?
    
    init(Doc: DocumentSnapshot) {
        self.id = Doc.documentID
        self.userID = Doc.get("userID") as? String ?? ""
        self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
        self.company = Doc.get("company") as? NSDictionary
        self.content = Doc.get("content") as? String ?? ""
        
    }
}

I wrote in my view controller for fetch this,

var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
    super.viewDidLoad()
    let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
    docRef.getDocuments { (querysnapshot, error) in
        if let doc = querysnapshot?.documents, !doc.isEmpty {
            print("Document is present.")
            for document in querysnapshot!.documents {
                _ = document.documentID
                if let compCode = document.get("company") as? NSDictionary {
                    do {
                        let jsonData = try JSONSerialization.data(withJSONObject: compCode)
                        let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
                        if companyPost.companyCode == AuthService.instance.companyId ?? ""{
                            print(AuthService.instance.companyId ?? "")
                            if (document.get("postiiDetails") as? NSDictionary) != nil {
                                let commentItem = PostiisCollection(Doc: document)
                                self.postiisCollectionDetails.append(commentItem)   
                            }
                        }
                        
                    } catch {
                        print(error.localizedDescription)
                    }
                    DispatchQueue.main.async {
                        
                        self.tableView.isHidden = false
                        self.tableView.reloadData()
                    }
                }
            }
        }
    }
}

I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,

extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return postiisCollectionDetails.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
        let label1 = cell.viewWithTag(1) as? UILabel
        let imagePointer = cell.viewWithTag(3) as? UIImageView
        let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
        let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
        let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
        
        label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
        if getGif != nil {
            let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
            print(arrGif[0])
            
            let gifURL : String = "\(arrGif[0])"
            let imageURL = UIImage.gifImageWithURL(gifURL)
            imagePointer?.image = imageURL
            playButton?.isHidden = true
        }
        if getPhoto != nil  {
            let arrPhoto = getPhoto?.value(forKey: "photoUrl")  as! [String]
            print(arrPhoto[0])
            let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
            storageRef.downloadURL(completion: { (url, error) in
                do {
                    let data = try Data(contentsOf: url!)
                    let image = UIImage(data: data as Data)
                    DispatchQueue.main.async {
                        imagePointer?.image = image
                        playButton?.isHidden = true
                    }
                } catch {
                    print(error)
                }
            })
        }
        if getVideo != nil {
            let arrVideo = getVideo?.value(forKey: "videoUrl")  as! [String]
            
            let videoURL = URL(string: arrVideo[0])
            let asset = AVAsset(url:videoURL!)
            if let videoThumbnail = asset.videoThumbnail{
                SVProgressHUD.dismiss()
                imagePointer?.image = videoThumbnail
                playButton?.isHidden = false
            }
        }
    }
}

If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion

4 Answers

As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.

Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction. So for example:

// Add instance variables for fast access to data
private var images = [UIImage]() 
private var thumbnails = [UIImage]()

private func fetchData(completion: ()->()) {
    // Load storage URLs
    var storageURLs = ...

    // Load data from firebase
    let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
    storageRef.downloadURL(completion: { (url, error) in
         // Parse data and store resulting image in image array
         ...

         // Call completion handler to indicate that loading has finished
         completion()             
     })
}

Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.

This approach simplifies your cellForRowAtIndexPath method. There you can just say:

imagePointer?.image = ...Correct image from image array...

Without any background loading.

I suggest using below lightweight extension for image downloading from URL using NSCache

extension UIImageView {
 
    func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
        
        let imageCache = NSCache<NSString, UIImage>()

        DispatchQueue.main.async {[weak self] in
            self?.image = nil
        }
        
        if let image = imageCache.object(forKey: urlString as NSString) {
            DispatchQueue.main.async {[weak self] in
                self?.image = image
            }
            success?(image)
        } else {
            guard let url = URL(string: urlString) else {
                print("failed to create url")
                return
            }
            
            let request = URLRequest(url: url)
            let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
                
                // response received, now switch back to main queue
                DispatchQueue.main.async {[weak self] in
                    if let error = error {
                        failure?(error.localizedDescription)
                    }
                    else if let data = data, let image = UIImage(data: data) {
                        imageCache.setObject(image, forKey: url.absoluteString as NSString)
                        self?.image = image
                        success?(image)
                    } else {
                        failure?("Image not available")
                    }
                }
            }
            
            task.resume()
        }
    }
}

Usage:

let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)

No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension

In your case:

You can implement following logic in cellForRowAt method

if getGif != nil {
    let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
    let urlString : String = "\(arrGif[0])"
    let image = UIImage.gifImageWithURL(urlString)
    imagePointer?.image = image
    playButton?.isHidden = true
}
else if getPhoto != nil  {
    let arrPhoto = getPhoto?.value(forKey: "photoUrl")  as! [String]
    let urlString = Storage.storage().reference(forURL: arrPhoto[0])
    imagePointer?.downloadImage(urlString: urlString)
    playButton?.isHidden = true
}
elseif getVideo != nil {
    let arrVideo = getVideo?.value(forKey: "videoUrl")  as! [String]
    let urlString = arrVideo[0]
    imagePointer?.downloadImage(urlString: urlString)
    playButton?.isHidden = false
}

You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.

Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.

You can use SDWebImage for gif as well.

Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.

If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.

Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,

var cacheImages = [UIImage]()

private func fetchData(completionBlock: () -> ()) {
      
        for (index, _) in postiisCollectionDetails.enumerated() {
          
       
            let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
            let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
            let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
            
                 if getGif != nil {
                 let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
                 let gifURL : String = "\(arrGif[0])"
                 self.randomList.append(gifURL)
                
                /////---------------------------
                let imageURL = UIImage.gifImageWithURL(gifURL)
                self.cacheImages.append(imageURL!)
                //////=================
                
                }
                else if getVideo != nil {
                    let arrVideo = getVideo?.value(forKey: "videoUrl")  as! [String]
                    let videoURL: String = "\(arrVideo[0])"
                 let videoUrl = URL(string: arrVideo[0])
                 let asset = AVAsset(url:videoUrl!)
                 if let videoThumbnail = asset.videoThumbnail{
                 ////--------------
                 self.cacheImages.append(videoThumbnail)
                 //-----------
                 }
                 self.randomList.append(videoURL)

                }else if getPhoto != nil  {
                  let arrPhoto = getPhoto?.value(forKey: "photoUrl")  as! [String]
                  let photoURL : String = "\(arrPhoto[0])"
            
                /////---------------------------
                    let url = URL(string: photoURL)
                    let data = try? Data(contentsOf: url!)

                    if let imageData = data {
                        let image = UIImage(data: imageData)
                        if image != nil {
                         self.cacheImages.append(image!)
                        }
                        else {
                        let defaultImage: UIImage = UIImage(named:"edit-user-80")!
                        self.cacheImages.append(defaultImage)
                        }
                    }
                  
                 //////=================
                 
                 }
                 else {
                  //-----------------
                 let defaultImage: UIImage = UIImage(named:"edit-user-80")!
                self.cacheImages.append(defaultImage)
                
                //--------------------
                
                }
                
            }
        completionBlock()
 
    }

After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,

override func viewDidLoad() {
    self.fetchData {
       DispatchQueue.main.async 
        self.tableView.reloadData()
      }
     }
}

Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
  
          if getVideo != nil {
              imagePointer?.image = cacheImages[indexPath.row]
               playButton?.isHidden = false
           }else {
                imagePointer?.image = cacheImages[indexPath.row]
               // or  get photo with string via SdWebImage
              //   imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
                playButton?.isHidden = true
                }
return cell
}
Related