Skip to content

Instantly share code, notes, and snippets.

@Martini024
Last active July 19, 2025 22:14
Show Gist options
  • Save Martini024/9d84c9cd230ab171b9fd054035f5c260 to your computer and use it in GitHub Desktop.
Save Martini024/9d84c9cd230ab171b9fd054035f5c260 to your computer and use it in GitHub Desktop.
SwiftUI: Rewrite iOS Photos Video Scrubber
import Foundation
import AVKit
class VideoHelper {
static func getThumbnail(from player: AVPlayer, at time: CMTime) -> CGImage? {
do {
guard let currentItem = player.currentItem else { return nil }
let asset = currentItem.asset
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil)
return cgImage
} catch _ {
return nil
}
}
static func getThumbnail(from asset: AVAsset?, at time: CMTime) -> CGImage? {
do {
guard let asset = asset else { return nil }
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil)
return cgImage
} catch _ {
return nil
}
}
static func generateThumbnailImages(_ player: AVPlayer, _ containerSize: CGSize) -> [UIImage] {
var images: [UIImage] = []
guard let currentItem = player.currentItem else { return images }
guard let track = player.currentItem?.asset.tracks(withMediaType: AVMediaType.video).first else { return images }
let assetSize = track.naturalSize.applying(track.preferredTransform)
let height = containerSize.height
let ratio = assetSize.width / assetSize.height
let width = height * ratio
let thumbnailCount = Int(ceil(containerSize.width / abs(width)))
let interval = currentItem.asset.duration.seconds / Double(thumbnailCount)
for i in 0..<thumbnailCount {
guard let thumbnail = VideoHelper.getThumbnail(from: currentItem.asset, at: CMTime(seconds: Double(i) * interval, preferredTimescale: 1000)) else { return images }
images.append(UIImage(cgImage: thumbnail))
}
return images
}
static func getVideoAspectRatio(_ player: AVPlayer) -> CGFloat? {
guard let track = player.currentItem?.asset.tracks(withMediaType: AVMediaType.video).first else { return nil}
let assetSize = track.naturalSize.applying(track.preferredTransform)
return assetSize.width / assetSize.height
}
static func getCurrentTime(_ player: AVPlayer) -> CMTime? {
guard let currentItem = player.currentItem else { return nil }
return currentItem.currentTime()
}
static func getDuration(_ player: AVPlayer) -> CMTime? {
guard let currentItem = player.currentItem else { return nil }
return currentItem.asset.duration
}
}
import SwiftUI
import AVKit
struct VideoPlayerControls: View {
let player: AVPlayer
@Binding var currentTime: CGFloat
var height: CGFloat = 50
var actionImage: String = "plus"
@State private var isPlaying: Bool = false
@State private var isTracking: Bool = false
@State private var timeObserver: Any?
var action: (() -> Void)?
var body: some View {
HStack(spacing: 0) {
Button {
isPlaying ? player.pause() : player.play()
isPlaying.toggle()
} label: {
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.resizable()
.padding()
.frame(width: height, height: height, alignment: .center)
}
.foregroundColor(.white)
.overlay(Rectangle().frame(width: 1, height: nil).foregroundColor(Color.black), alignment: .trailing)
VideoScrollPreview(player: player, isPlaying: $isPlaying, currentTime: $currentTime, isTracking: $isTracking)
.padding(4)
.frame(width: nil, height: height)
if let action = action {
Button {
action()
} label: {
Image(systemName: actionImage)
.resizable()
.padding()
.frame(width: height, height: height, alignment: .center)
}
.foregroundColor(.white)
.overlay(Rectangle().frame(width: 1, height: nil).foregroundColor(Color.black), alignment: .leading)
}
}
.background(Color.darkGray)
.cornerRadius(5)
.onAppear {
startPeriodicTimeObserver()
}
.onDisappear {
stopPeriodicTimeObserver()
}
}
func startPeriodicTimeObserver() {
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: nil) { time in
guard isTracking == false else { return }
guard let duration = VideoHelper.getDuration(player) else { return }
self.currentTime = CGFloat(CMTimeGetSeconds(time) / CMTimeGetSeconds(duration))
if self.currentTime == 1.0 {
self.isPlaying = false
}
}
}
func stopPeriodicTimeObserver() {
guard let observer = timeObserver else { return }
player.removeTimeObserver(observer)
}
}
import SwiftUI
import AVKit
struct VideoScrollPreview: View {
let player: AVPlayer
@Binding var isPlaying: Bool
@Binding var currentTime: CGFloat
@Binding var isTracking: Bool
@State private var images: [UIImage] = []
var body: some View {
GeometryReader { geometry in
ZStack {
HStack(spacing: 0) {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFit()
}
}
RoundedRectangle(cornerRadius: 10, style: .continuous)
.frame(width: 4, height: geometry.size.height + 4)
.position(x: currentTime * geometry.size.width, y: geometry.size.height / 2)
.foregroundColor(.white)
.shadow(radius: 10)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged({
isTracking = true
if isPlaying {
player.pause()
}
currentTime = min(geometry.size.width, max(0, $0.location.x)) / geometry.size.width
guard let duration = VideoHelper.getDuration(player) else { return }
let targetTime = CMTimeMultiplyByFloat64(duration, multiplier: Float64(currentTime))
player.seek(to: targetTime)
})
.onEnded({ _ in
isTracking = false
if isPlaying {
player.play()
}
})
)
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
.onAppear {
images = VideoHelper.generateThumbnailImages(player, geometry.size)
}
}
}
}
@donrodriguez
Copy link

donrodriguez commented Mar 21, 2024

THANK YOU SO MUCH!!! You freaking genius

@lordzsolt
Copy link

I recommend setting imgGenerator.maximumSize = size using the width/height values in generateThumbnailImages, otherwise it will generate full size images, which was like 60MB each for me 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment