Search code examples
swiftswiftuiavfoundation

AVPlayerLayer's frame size update in SwiftUI


I have a view which asynchronously loads a video and presents it via another UIViewRepresentable view. GeometryReader provides the video container's frame which is then used to initialize an AVPlayerLayer. A problem arises when the device orientation is changed (thus, the container's frame is changed too), and AVPlayerLayer doesn't relayout itself to match the container. I tried to update the layer's frame within updateUIView, even call setNeedsLayout and setNeedsDisplay to both the layer and uiView, but nothing changed.

How to adapt the layer to match the size of the container?

struct VideoView: View {

    @StateObject var videoLoader: VideoLoader

    @ViewBuilder
    var body: some View {
        if let player = videoLoader.player {
            GeometryReader { geometry in
                let frame = geometry.frame(in: .local)
                VideoPlayerView(frame: frame, player: player)
                    .frame(width: frame.width, height: frame.height)
                    .onAppear { player.play() }
                    .onDisappear { player.pause() }
            }
        }
    }

}

struct VideoPlayerView: UIViewRepresentable {

    private let frame: CGRect
    private let playerLayer: AVPlayerLayer

    init(frame: CGRect, player: AVPlayer) {
        self.frame = frame
        self.playerLayer = AVPlayerLayer(player: player)
        self.playerLayer.frame = frame
    }

    func makeUIView(context: Context) -> UIView {
        let container = UIView()
        container.layer.addSublayer(playerLayer)
        return container
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        playerLayer.frame.size = uiView.frame.size
    }

}

Solution

  • The solution is to create a custom UIView and update the layer's frame size in layoutSubviews().

    struct VideoPlayerView: UIViewRepresentable {
    
        class PlayerContainer: UIView {
    
            init(playerLayer: AVPlayerLayer) {
                super.init(frame: .zero)
                layer.addSublayer(playerLayer)
            }
    
            required init?(coder: NSCoder) {
                fatalError("init(coder:) has not been implemented")
            }
    
            override func layoutSubviews() {
                super.layoutSubviews()
                layer.sublayers?.first?.frame = frame
            }
    
        }
    
        private let playerLayer: AVPlayerLayer
    
        init(player: AVPlayer) {
            playerLayer = AVPlayerLayer(player: player)
        }
    
        func makeUIView(context: Context) -> UIView {
            return PlayerContainer(playerLayer: playerLayer)
        }
    
        func updateUIView(_ uiView: UIView, context: Context) { }
    
    }