You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift AVKit宽高比适配异常 PlayerView外边框显示为正方形

问题根因

边框和视频区域错位的核心原因:

  • .scaledToFit()仅调整视图内部内容的渲染缩放规则,不会修改PlayerView本身的实际布局尺寸
  • 仅给视图固定200高度时,SwiftUI默认给UIViewRepresentable封装的组件分配正方形可用空间
  • AVPlayerLayer设置videoGravity = .resizeAspect后,只会在图层内部按比例缩放视频,整个LoopingPlayerUIView的bounds依然是正方形,因此overlay绘制的红色边框会沿着正方形视图边缘绘制,和内部实际显示的视频区域错位。
修复方案

核心思路是让PlayerView的实际布局尺寸严格匹配视频的宽高比,从根源上消除视图尺寸和视频显示区域的差值,边框自然会贴合视频边缘。

完整实现代码

  1. 修改自定义播放器组件,增加视频实际尺寸的回调逻辑,同时修正视频旋转带来的尺寸计算偏差:
import SwiftUI
import AVKit

// 视频比例更新通知名
extension Notification.Name {
    static let videoAspectRatioLoaded = Notification.Name("videoAspectRatioLoaded")
}

struct PlayerView: UIViewRepresentable {
    let forResource: String
    let withExtension: String
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    func makeUIView(context: Context) -> LoopingPlayerUIView {
        let playerView = LoopingPlayerUIView(
            frame: .zero,
            forResource: forResource,
            withExtension: withExtension
        )
        playerView.onVideoDimensionLoad = { [weak coordinator = context.coordinator] ratio in
            coordinator?.updateRatio(ratio)
        }
        return playerView
    }
    
    func updateUIView(_ uiView: LoopingPlayerUIView, context: Context) {}
    
    class Coordinator {
        func updateRatio(_ ratio: CGFloat) {
            NotificationCenter.default.post(
                name: .videoAspectRatioLoaded,
                object: ratio
            )
        }
    }
}

class LoopingPlayerUIView: UIView {
    private let playerLayer = AVPlayerLayer()
    private var playerLooper: AVPlayerLooper?
    var onVideoDimensionLoad: ((CGFloat) -> Void)?
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    init(frame: CGRect, forResource: String, withExtension: String) {
        super.init(frame: frame)
        // 加载资源
        guard let fileUrl = Bundle.main.url(forResource: forResource, withExtension: withExtension) else {
            return
        }
        let asset = AVAsset(url: fileUrl)
        let playerItem = AVPlayerItem(asset: asset)
        
        // 异步获取视频实际尺寸,处理旋转偏移
        asset.loadTracks(withMediaType: .video) { [weak self] tracks, error in
            guard let self = self,
                  let videoTrack = tracks?.first,
                  error == nil else { return }
            let naturalSize = videoTrack.naturalSize
            let transform = videoTrack.preferredTransform
            let transformedSize = naturalSize.applying(transform)
            let correctedSize = CGSize(
                width: abs(transformedSize.width),
                height: abs(transformedSize.height)
            )
            let aspectRatio = correctedSize.width / correctedSize.height
            DispatchQueue.main.async {
                self.onVideoDimensionLoad?(aspectRatio)
            }
        }
        
        // 初始化播放器
        let queuePlayer = AVQueuePlayer()
        playerLayer.player = queuePlayer
        playerLayer.videoGravity = .resizeAspect
        layer.addSublayer(playerLayer)
        
        playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem)
        queuePlayer.play()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }
}
  1. 调用层监听视频比例,给视图设置匹配的宽高比,再添加圆角和边框:
struct ContentView: View {
    @State private var videoRatio: CGFloat = 1
    
    var body: some View {
        PlayerView(forResource: "x", withExtension: "mp4")
            .frame(height: 200)
            .aspectRatio(videoRatio, contentMode: .fit)
            .cornerRadius(20)
            .overlay {
                RoundedRectangle(cornerRadius: 20)
                    .stroke(Color.red, lineWidth: 5)
            }
            .onReceive(NotificationCenter.default.publisher(for: .videoAspectRatioLoaded)) { output in
                guard let ratio = output.object as? CGFloat else { return }
                videoRatio = ratio
            }
    }
}

效果说明

  • 视频资源加载完成后会自动计算真实宽高比,兼容横竖屏拍摄的视频
  • PlayerView的实际布局会自动匹配视频比例,不会再出现多余的正方形空白区域
  • 圆角、边框会完全贴合视频显示边缘,不会出现错位

注意:如果需要固定宽度而非高度,只需要把.frame(height: 200)替换成.frame(width: 目标宽度)即可,aspectRatio会自动计算对应高度,边框依然会正确贴合。


内容的提问来源于stack exchange,提问作者nickcoding2

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 23:27:12