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

如何修改基于UIKit的AVPlayerView以适配Apple Watch?

适配Apple Watch的视频播放代码修改方案

核心问题分析

现有代码依赖UIKit框架(UIViewRepresentable、PlayerView继承自UIView),而Apple Watch使用WatchKit框架,无法直接兼容UIKit组件,需要通过平台条件编译分别实现iOS和WatchOS的适配层。


分步修改方案

1. 重构CustomVideoPlayer实现跨平台适配

通过条件编译分别实现iOS的UIViewRepresentable和WatchOS的WKInterfaceObjectRepresentable:

import SwiftUI
import Combine
import AVKit

// iOS平台专属播放器视图
#if os(iOS)
public struct CustomVideoPlayer: UIViewRepresentable {
  @ObservedObject private var playerVM: PlayerViewModel
  
  public init(_ playerVM: PlayerViewModel) {
    self.playerVM = playerVM
  }
  
  public func makeUIView(context: Context) -> PlayerView {
    let view = PlayerView()
    view.player = playerVM.player
    context.coordinator.setController(view.playerLayer)
    return view
  }
  
  public func updateUIView(_ uiView: PlayerView, context: Context) { }
  
  public func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  public class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
    private let parent: CustomVideoPlayer
    private var controller: AVPictureInPictureController?
    private var cancellable: AnyCancellable?
    
    init(_ parent: CustomVideoPlayer) {
      self.parent = parent
      super.init()
      
      cancellable = parent.playerVM.$isInPipMode
        .sink { [weak self] isPip in
          guard let self = self, let controller = self.controller else { return }
          if isPip, !controller.isPictureInPictureActive {
            controller.startPictureInPicture()
          } else if !isPip, controller.isPictureInPictureActive {
            controller.stopPictureInPicture()
          }
        }
    }
    
    public func setController(_ playerLayer: AVPlayerLayer) {
      controller = AVPictureInPictureController(playerLayer: playerLayer)
      controller?.canStartPictureInPictureAutomaticallyFromInline = true
      controller?.delegate = self
    }
    
    public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
      parent.playerVM.isInPipMode = true
    }
    
    public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
      parent.playerVM.isInPipMode = false
    }
  }
}
#endif

// WatchOS平台专属播放器视图
#if os(watchOS)
public struct CustomVideoPlayer: WKInterfaceObjectRepresentable {
  @ObservedObject private var playerVM: PlayerViewModel
  
  public init(_ playerVM: PlayerViewModel) {
    self.playerVM = playerVM
  }
  
  public func makeWKInterfaceObject(context: Context) -> WKInterfaceMovie {
    let movie = WKInterfaceMovie()
    movie.setPlayer(playerVM.player)
    movie.setVideoGravity(.resizeAspectFill)
    return movie
  }
  
  public func updateWKInterfaceObject(_ movie: WKInterfaceMovie, context: Context) { }
  
  public func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  public class Coordinator: NSObject {
    private let parent: CustomVideoPlayer
    
    init(_ parent: CustomVideoPlayer) {
      self.parent = parent
      super.init()
    }
  }
}
#endif

2. 修改PlayerViewModel移除WatchOS不支持的功能

WatchOS没有画中画(PIP)功能,通过条件编译屏蔽相关属性:

import AVFoundation
import Combine

final public class PlayerViewModel: ObservableObject {
  public let player = AVPlayer()
  #if os(iOS)
  @Published var isInPipMode: Bool = false
  #endif
  @Published var isPlaying = false
  
  @Published var isEditingCurrentTime = false
  @Published var currentTime: Double = .zero
  @Published var duration: Double?
  
  private var subscriptions: Set<AnyCancellable> = []
  private var timeObserver: Any?
  
  deinit {
    if let timeObserver = timeObserver {
      player.removeTimeObserver(timeObserver)
    }
  }
  
  public init() {
    $isEditingCurrentTime
      .dropFirst()
      .filter({ !$0 })
      .sink(receiveValue: { [weak self] _ in
        guard let self = self else { return }
        self.player.seek(to: CMTime(seconds: self.currentTime, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero)
        if self.player.rate != 0 {
          self.player.play()
        }
      })
      .store(in: &subscriptions)
    
    player.publisher(for: \.timeControlStatus)
      .sink { [weak self] status in
        guard let self = self else { return }
        switch status {
        case .playing:
          self.isPlaying = true
        case .paused:
          self.isPlaying = false
        case .waitingToPlayAtSpecifiedRate:
          break
        @unknown default:
          break
        }
      }
      .store(in: &subscriptions)
    
    timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { [weak self] time in
      guard let self = self else { return }
      if !self.isEditingCurrentTime {
        self.currentTime = time.seconds
      }
    }
  }
  
  private func videoURL(_ filename: String) -> URL? {
    Bundle.main.url(forResource: filename, withExtension: "mp4")
  }
  
  public func setCurrentItem(_ filename: String){
    currentTime = .zero
    duration = nil
    
    guard let videoURL = videoURL(filename) else { return }
    
    let avPlayerItem = AVPlayerItem(url: videoURL)
    player.replaceCurrentItem(with: avPlayerItem)
    
    avPlayerItem.publisher(for: \.status)
      .filter({ $0 == .readyToPlay })
      .sink(receiveValue: { [weak self] _ in
        self?.duration = avPlayerItem.asset.duration.seconds
      })
      .store(in: &subscriptions)
  }
}

3. 调整PlayerView仅在iOS编译

将原PlayerView用iOS专属编译条件包裹,避免WatchOS编译错误:

#if os(iOS)
import AVFoundation
import UIKit

final public class PlayerView: UIView {
  public override static var layerClass: AnyClass {
    AVPlayerLayer.self
  }
  
  var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
  
  var player: AVPlayer? {
    get { playerLayer.player }
    set {
      playerLayer.videoGravity = .resizeAspectFill
      playerLayer.player = newValue
    }
  }
}
#endif

4. 适配CustomControlsView的WatchOS界面

针对WatchOS小屏幕调整控件尺寸:

import SwiftUI

public struct CustomControlsView: View {
  @ObservedObject private var playerVM: PlayerViewModel
  
  public init(playerVM: PlayerViewModel) {
    self.playerVM = playerVM
  }
  
  // 根据平台设置按钮尺寸
  private let buttonSize: CGFloat = {
    #if os(iOS)
    return 25
    #elseif os(watchOS)
    return 20
    #endif
  }()
  
  public var body: some View {
    HStack {
      if !playerVM.isPlaying {
        Button(action: { playerVM.player.play() }) {
          Image(systemName: "play.circle")
            .renderingMode(.template)
            .font(.system(size: buttonSize))
            .foregroundColor(.black)
        }
      } else {
        Button(action: { playerVM.player.pause() }) {
          Image(systemName: "pause.circle")
            .renderingMode(.template)
            .font(.system(size: buttonSize))
            .foregroundColor(.black)
        }
      }
      
      if let duration = playerVM.duration {
        Slider(value: $playerVM.currentTime, in: 0...duration, onEditingChanged: { isEditing in
          playerVM.isEditingCurrentTime = isEditing
        })
        #if os(watchOS)
        .frame(height: 10) // WatchOS下调整Slider高度
        #endif
      } else {
        Spacer()
      }
    }
    .padding()
    .background(.thinMaterial)
  }
}

使用方式

原有使用代码无需修改,条件编译会自动根据平台加载对应实现:

CustomVideoPlayer(playerVM)
  .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
  .onAppear {
    playerVM.setCurrentItem("beach")
    playerVM.player.play()
  }

关键修改说明

  • 平台隔离:通过#if os()编译指令,将UIKit和WatchKit的代码完全隔离,避免跨平台编译冲突。
  • WatchOS播放器选型:使用WKInterfaceMovie作为WatchOS的视频载体,这是Apple官方提供的适配组件,原生支持AVPlayer。
  • 功能裁剪:移除WatchOS不支持的画中画功能,避免无效代码执行。
  • 界面适配:针对WatchOS的小屏幕调整控件尺寸,保证交互可用性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:45:55