skip to Main Content

I am trying to recreate this thing. I’ve created in Storyboard skeleton. Here’s the idea of my code:

  1. Fetch images from URL’s array with help of the function getThumbnailFromImage
  2. Add UIImage’s with my thumbnails in array webImages
  3. Add in ViewController reusable cell MyCollectionView

But here I am with this))) (Don’t mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don’t know where to put it.

ViewController:

//
//  ViewController.swift
//  youtube-clone
//
//  Created by мас on 16.08.2022.
//

import Foundation
import UIKit
import YouTubePlayer
import AVFoundation

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var url: [URL?] = [
        URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
        URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
        URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
        URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
    ]
    
    var webImages: [UIImage] = []
    
    var currentPage: Int = 0
    
    @IBOutlet var myPage: UIPageControl!
    
    @IBOutlet weak var buttonInfo: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupLayout()
        
        myPage.currentPage = 0
        myPage.numberOfPages = webImages.count
        
    }
    
    // MARK: - Collection View Setup
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return webImages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
         
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
        
        getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
            self.webImages.append(image!)
        })
        
        cell.myWebImage.image = webImages[indexPath.row]
        
        cell.myWebImage.layer.cornerRadius = 20
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        myPage.currentPage = indexPath.row
    }
    
    // MARK: - Layout Setup // IGNORE IT
    
    func setupLayout() {
        
        buttonInfo.layer.cornerRadius = 25
        
        buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
        
        
        self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
    }
    
    // MARK: - Videos Thumbnail Fetcher
    func getThumbnailFromImage(url: URL, completion: @escaping ((_ image: UIImage?) -> Void)) {
        
        DispatchQueue.global().async {
            let asset = AVAsset(url: url)
            let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
            avAssetImageGenerator.appliesPreferredTrackTransform = true
            
            let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
            do {
                let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
                let thumbImage = UIImage(cgImage: cgThumbImage)
                
                DispatchQueue.main.async {
                    completion(thumbImage)
                }
            }
            catch {
                print(error.localizedDescription)
            }
        }
        
    }
}


Reusable Cell AKA MyCollectionCell:

import UIKit

class MyCollectionCell: UICollectionViewCell {
    
    @IBOutlet var myWebImage: UIImageView!
    
}

P.s.: YouTubePlayer is custom pod from GitHub, it’s not currently used.

2

Answers


  1. A couple of thoughts:

    1. @matt is right in the comment – getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.

    2. From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn’t even be reached. (I’m not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I’m assuming you’re populating webImages somewhere else before the collection view gets laid out).

    One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:

    class MyCollectionCell: UICollectionViewCell {
    
        @IBOutlet var myWebImage: UIImageView!
    
        func configure(urlString: String) {
           guard let self = self, let url = URL(string: urlString) else {
               return
           }
    
           getThumbnailFromImage(url: url, completion: { [weak self] image in
               self?.myWebImage.image = image
           })
        }
    
        // Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
    }
    

    The cellForItemAt function in the VC would need to be changed to something like this:

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
         
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
        
        cell.configure(urlString: url[indexPath.row])
                
        cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
        
        return cell
    }
    

    An added benefit of this approach is that you’re not referencing a separate array of images that could theoretically end up being in the wrong order if there’s a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead – or eventually the number of elements returned from an API somewhere.

    Side note – make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it’s been deallocated! Currently the call to getThumbnailFromImage doesn’t have that 🙂

    Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.

    Login or Signup to reply.
  2. You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
    and each YouTube video has four generated images.

    https://img.youtube.com/vi/{id}/0.jpg
    https://img.youtube.com/vi/{id}/1.jpg
    https://img.youtube.com/vi/{id}/2.jpg
    https://img.youtube.com/vi/{id}/3.jpg
    

    Example
    https://img.youtube.com/vi/KhebpuFBD14/0.jpg

    And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search