Swift AVKit宽高比适配异常 PlayerView外边框显示为正方形
问题根因
边框和视频区域错位的核心原因:
.scaledToFit()仅调整视图内部内容的渲染缩放规则,不会修改PlayerView本身的实际布局尺寸- 仅给视图固定200高度时,SwiftUI默认给
UIViewRepresentable封装的组件分配正方形可用空间 AVPlayerLayer设置videoGravity = .resizeAspect后,只会在图层内部按比例缩放视频,整个LoopingPlayerUIView的bounds依然是正方形,因此overlay绘制的红色边框会沿着正方形视图边缘绘制,和内部实际显示的视频区域错位。
修复方案
核心思路是让PlayerView的实际布局尺寸严格匹配视频的宽高比,从根源上消除视图尺寸和视频显示区域的差值,边框自然会贴合视频边缘。
完整实现代码
- 修改自定义播放器组件,增加视频实际尺寸的回调逻辑,同时修正视频旋转带来的尺寸计算偏差:
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 } }
- 调用层监听视频比例,给视图设置匹配的宽高比,再添加圆角和边框:
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
相关产品推荐
相关产品推荐

