I am trying to recreate this thing. I’ve created in Storyboard skeleton. Here’s the idea of my code:
- Fetch images from URL’s array with help of the function getThumbnailFromImage
- Add UIImage’s with my thumbnails in array webImages
- 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
A couple of thoughts:
@matt is right in the comment –
getThumbnailFromImage
will likely not have called the completion block by the timecellForItemAt
returns.From what is visible in the code you posted,
webImages.count
will still be 0 when your collection view checksnumberOfItemsInSection
. If the number of items is 0,cellForItemAt
may never get called so the call togetThumbnailFromImage
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 populatingwebImages
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:
The
cellForItemAt
function in the VC would need to be changed to something like this: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 useurls.count
innumberOfItemsInSection
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 referencesself
to avoid trying to access it after it’s been deallocated! Currently the call togetThumbnailFromImage
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.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.
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.