iOS development
Swift
asynchronous programming
UITableView
video thumbnail loading

How to load video url's thumbnail image on tableview list Asynchronously

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Loading video thumbnails asynchronously in a UITableView is essential for a smooth scrolling experience. Extracting a thumbnail from a video URL is a heavy operation that involves downloading video metadata over the network, so it must be done off the main thread. This article covers how to generate thumbnails from video URLs using AVAssetImageGenerator, cache them for performance, and display them correctly in a table view.

Generating a Thumbnail from a Video URL

Use AVAssetImageGenerator to extract a frame from a video:

swift
1import AVFoundation
2import UIKit
3
4func generateThumbnail(from url: URL, completion: @escaping (UIImage?) -> Void) {
5    DispatchQueue.global(qos: .userInitiated).async {
6        let asset = AVURLAsset(url: url)
7        let generator = AVAssetImageGenerator(asset: asset)
8        generator.appliesPreferredTrackTransform = true
9        generator.maximumSize = CGSize(width: 320, height: 180)
10
11        let time = CMTime(seconds: 1.0, preferredTimescale: 600)
12
13        do {
14            let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
15            let thumbnail = UIImage(cgImage: cgImage)
16            DispatchQueue.main.async {
17                completion(thumbnail)
18            }
19        } catch {
20            print("Thumbnail generation failed: \(error)")
21            DispatchQueue.main.async {
22                completion(nil)
23            }
24        }
25    }
26}

Async/Await Version (iOS 16+)

swift
1func generateThumbnail(from url: URL) async -> UIImage? {
2    let asset = AVURLAsset(url: url)
3    let generator = AVAssetImageGenerator(asset: asset)
4    generator.appliesPreferredTrackTransform = true
5    generator.maximumSize = CGSize(width: 320, height: 180)
6
7    let time = CMTime(seconds: 1.0, preferredTimescale: 600)
8
9    do {
10        let (cgImage, _) = try await generator.image(at: time)
11        return UIImage(cgImage: cgImage)
12    } catch {
13        print("Thumbnail generation failed: \(error)")
14        return nil
15    }
16}

Adding a Thumbnail Cache

Without caching, scrolling the table view triggers repeated thumbnail generation for the same videos, wasting bandwidth and CPU:

swift
1class ThumbnailCache {
2    static let shared = ThumbnailCache()
3    private let cache = NSCache<NSString, UIImage>()
4
5    init() {
6        cache.countLimit = 100
7        cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
8    }
9
10    func thumbnail(for url: URL, completion: @escaping (UIImage?) -> Void) {
11        let key = url.absoluteString as NSString
12
13        // Check cache first
14        if let cached = cache.object(forKey: key) {
15            completion(cached)
16            return
17        }
18
19        // Generate and cache
20        generateThumbnail(from: url) { [weak self] image in
21            if let image = image {
22                self?.cache.setObject(image, forKey: key)
23            }
24            completion(image)
25        }
26    }
27
28    func clearCache() {
29        cache.removeAllObjects()
30    }
31}

UITableView Implementation

Data Model

swift
1struct Video {
2    let title: String
3    let url: URL
4}

Table View Cell

swift
1class VideoCell: UITableViewCell {
2    @IBOutlet weak var thumbnailImageView: UIImageView!
3    @IBOutlet weak var titleLabel: UILabel!
4
5    private var currentURL: URL?
6
7    override func prepareForReuse() {
8        super.prepareForReuse()
9        thumbnailImageView.image = nil
10        currentURL = nil
11    }
12
13    func configure(with video: Video) {
14        titleLabel.text = video.title
15        currentURL = video.url
16
17        // Show placeholder
18        thumbnailImageView.image = UIImage(named: "video_placeholder")
19
20        // Load thumbnail asynchronously
21        ThumbnailCache.shared.thumbnail(for: video.url) { [weak self] image in
22            guard let self = self,
23                  self.currentURL == video.url else {
24                return // Cell was reused for a different video
25            }
26            self.thumbnailImageView.image = image ?? UIImage(named: "video_placeholder")
27        }
28    }
29}

View Controller

swift
1class VideoListViewController: UITableViewController {
2    var videos: [Video] = [
3        Video(title: "Introduction", url: URL(string: "https://example.com/video1.mp4")!),
4        Video(title: "Advanced Topics", url: URL(string: "https://example.com/video2.mp4")!),
5        // ...
6    ]
7
8    override func tableView(_ tableView: UITableView,
9                           numberOfRowsInSection section: Int) -> Int {
10        return videos.count
11    }
12
13    override func tableView(_ tableView: UITableView,
14                           cellForRowAt indexPath: IndexPath) -> UITableViewCell {
15        let cell = tableView.dequeueReusableCell(
16            withIdentifier: "VideoCell", for: indexPath
17        ) as! VideoCell
18        cell.configure(with: videos[indexPath.row])
19        return cell
20    }
21}

SwiftUI Version

swift
1import SwiftUI
2import AVFoundation
3
4struct VideoThumbnailView: View {
5    let url: URL
6    @State private var thumbnail: UIImage?
7
8    var body: some View {
9        Group {
10            if let thumbnail = thumbnail {
11                Image(uiImage: thumbnail)
12                    .resizable()
13                    .aspectRatio(16/9, contentMode: .fill)
14            } else {
15                Rectangle()
16                    .fill(Color.gray.opacity(0.3))
17                    .aspectRatio(16/9, contentMode: .fill)
18                    .overlay(ProgressView())
19            }
20        }
21        .task {
22            thumbnail = await generateThumbnail(from: url)
23        }
24    }
25}
26
27struct VideoListView: View {
28    let videos: [Video]
29
30    var body: some View {
31        List(videos, id: \.url) { video in
32            HStack {
33                VideoThumbnailView(url: video.url)
34                    .frame(width: 120, height: 68)
35                    .clipShape(RoundedRectangle(cornerRadius: 8))
36                Text(video.title)
37            }
38        }
39    }
40}

Prefetching Thumbnails

Use UITableViewDataSourcePrefetching to start loading thumbnails before cells appear:

swift
1extension VideoListViewController: UITableViewDataSourcePrefetching {
2    func tableView(_ tableView: UITableView,
3                   prefetchRowsAt indexPaths: [IndexPath]) {
4        for indexPath in indexPaths {
5            let video = videos[indexPath.row]
6            ThumbnailCache.shared.thumbnail(for: video.url) { _ in
7                // Just warming the cache
8            }
9        }
10    }
11}

Register the prefetch data source in viewDidLoad:

swift
tableView.prefetchDataSource = self

Common Pitfalls

  • Cell reuse without URL check: When a cell is dequeued and reused, the old thumbnail callback may fire and set the wrong image. Always compare the current URL in the completion handler (as shown in the configure method).
  • Main thread blocking: AVAssetImageGenerator.copyCGImage(at:) is synchronous and slow for remote URLs. Always call it on a background queue.
  • No placeholder image: Without a placeholder, the image view shows the previous cell's thumbnail briefly (from cell reuse), creating a "flashing" effect.
  • Memory pressure: Large thumbnail caches can cause memory warnings. Use NSCache (which auto-evicts under pressure) rather than a plain dictionary.
  • HLS/streaming URLs: AVAssetImageGenerator may not work with HLS (.m3u8) streams. For HLS content, use the video's poster image URL if available, or load the first segment.

Summary

  • Use AVAssetImageGenerator to extract video thumbnails on a background thread
  • Cache thumbnails with NSCache to avoid re-generating on scroll
  • Always check the current URL in the completion handler to prevent stale images in reused cells
  • Use placeholder images while thumbnails load
  • Implement UITableViewDataSourcePrefetching to start loading before cells appear

Course illustration
Course illustration

All Rights Reserved.