79690698

Date: 2025-07-04 22:43:21
Score: 0.5
Natty:
Report link

Fwiw I was able to both simplify the approach, improve the performance and refine the behaviour (thanks to Matt for getting me on the right track). Here's the full code in case it helps anybody else.

import SwiftUI
import UIKit
import AVFoundation

class VideoLayerView: UIView {
    private var playerLayer: AVPlayerLayer?
    private var player: AVPlayer?
    private var videoSize: CGSize = .zero
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupVideo()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupVideo()
    }
    
    private func setupVideo() {
        guard let videoURL = Bundle.main.url(forResource: "example", withExtension: "MP4") else {
            print("Could not find example.MP4 in bundle")
            return
        }
        
        let asset = AVURLAsset(url: videoURL)
        player = AVPlayer(playerItem: AVPlayerItem(asset: asset))
        playerLayer = AVPlayerLayer(player: player)
        
        Task {
            do {
                let tracks = try await asset.loadTracks(withMediaType: .video)
                if let videoTrack = tracks.first {
                    let size = try await videoTrack.load(.naturalSize)
                    await MainActor.run {
                        self.videoSize = size
                        self.updateVideoLayout()
                    }
                }
            } catch {
                print("Error loading video dimensions: \(error)")
            }
        }
        
        playerLayer?.videoGravity = .resizeAspect
        layer.addSublayer(playerLayer!)
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(playerDidFinishPlaying),
            name: .AVPlayerItemDidPlayToEndTime,
            object: player?.currentItem
        )
        
        player?.play()
    }
    
    @objc private func playerDidFinishPlaying() {
        print("Video finished playing")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        updateVideoLayout()
    }
    
    private func updateVideoLayout() {
        guard let playerLayer = playerLayer, videoSize.width > 0, videoSize.height > 0 else { return }
        
        let viewWidth = bounds.width
        let viewHeight = bounds.height
        let videoAspectRatio = videoSize.width / videoSize.height
        let viewAspectRatio = viewWidth / viewHeight
        
        var videoWidth: CGFloat
        var videoHeight: CGFloat
        
        if videoAspectRatio > viewAspectRatio {
            videoHeight = viewHeight
            videoWidth = viewHeight * videoAspectRatio
        } else {
            videoWidth = viewWidth
            videoHeight = viewWidth / videoAspectRatio
        }
        
        playerLayer.frame = CGRect(
            x: 0,
            y: 0,
            width: videoWidth,
            height: videoHeight
        )
    }
    
    func restartVideo() {
        player?.seek(to: .zero)
        player?.play()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

class SequenceView: UIView {
    private var videoLayerView: VideoLayerView!
    private var currentOrientation: UIDeviceOrientation = .unknown
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }
    
    private func setupView() {
        backgroundColor = .white
        
        videoLayerView = VideoLayerView()
        videoLayerView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(videoLayerView)
        
        NSLayoutConstraint.activate([
            videoLayerView.topAnchor.constraint(equalTo: topAnchor),
            videoLayerView.leadingAnchor.constraint(equalTo: leadingAnchor),
            videoLayerView.trailingAnchor.constraint(equalTo: trailingAnchor),
            videoLayerView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(orientationDidChange),
            name: UIDevice.orientationDidChangeNotification,
            object: nil
        )
        
        currentOrientation = UIDevice.current.orientation
    }
    
    @objc private func orientationDidChange() {
        let newOrientation = UIDevice.current.orientation
        
        if newOrientation != currentOrientation && newOrientation != .unknown {
            currentOrientation = newOrientation
            
            DispatchQueue.main.async { [weak self] in
                self?.videoLayerView.restartVideo()
            }
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

struct SequenceViewWrapper: UIViewRepresentable {
    func makeUIView(context: Context) -> SequenceView {
        return SequenceView()
    }
    
    func updateUIView(_ uiView: SequenceView, context: Context) {
    }
}

struct ContentView: View {
    var body: some View {
        SequenceViewWrapper()
            .ignoresSafeArea()
    }
}
Reasons:
  • Blacklisted phrase (0.5): thanks
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: gjh