我在将 SwiftUI
TabView
与 UIViewRepresentable
组合时遇到问题。
MobileVLCKit
和 UIViewRepresentable
自定义了视频/流播放器。TabView
的原因)。strokeView
,当选择某个项目时会出现,以指示当前选择了哪个项目。tabViewStyle
有关。tabViewStyle
时:单击视频项目不会显示 strokeView
。tabViewStyle
时:单击视频项目会按预期显示 strokeView
,但无法在选项卡之间滑动。struct Test2View: View {
var body: some View {
let cameras = [
CameraCommonItemData(name: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
CameraCommonItemData(name: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"),
CameraCommonItemData(name: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"),
CameraCommonItemData(name: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4"),
CameraCommonItemData(name: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4"),
]
CameraGridView(cameras: cameras)
}
}
struct CameraGridView: View {
let cameras: [CameraCommonItemData]
@State private var itemSelected: CameraCommonItemData?
@State private var dict: [String: CameraCommonItemData] = [:]
private let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1),
]
var width: CGFloat {
let deviceWidth = UIScreen.main.bounds.width
return deviceWidth / 2 - 1
}
var height: CGFloat {
return width / (16 / 9)
}
func overlay(_ camera: CameraCommonItemData) -> some View {
let isSelected = itemSelected?.id == camera.id
let overlay = AnyView(Rectangle()
.stroke(isSelected ? Color.red : Color.clear, lineWidth: 3)
.frame(width: width, height: height))
return overlay
}
var body: some View {
let chunks = cameras.chunked(into: 4)
TabView {
ForEach(chunks.indices, id: \.self) { index in
LazyVGrid(columns: columns, spacing: 1) {
ForEach(chunks[index]) { camera in
VLCItemView(store: camera.store, urlString: camera.name)
.frame(width: width, height: height)
.overlay(strokeView(camera))
.onTapGesture {
itemSelected = camera
}
.onAppear {
if self.dict[camera.id] == nil {
self.dict[camera.id] = camera
camera.store.loadURL(camera.name)
} else {
camera.store.play()
}
}.onDisappear {
camera.store.pause()
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
}
func strokeView(_ data: CameraCommonItemData) -> some View {
Rectangle()
.stroke(itemSelected == data ? Color.red : Color.clear, lineWidth: 3)
.frame(width: width, height: height)
}
}
struct CameraCommonItemData: Identifiable, Equatable {
let id = UUID().uuidString
let name: String
var store: CommonMonitoringCameraStore
static func == (lhs: CameraCommonItemData, rhs: CameraCommonItemData) -> Bool {
return lhs.id == rhs.id
}
init(name: String) {
self.name = name
self.store = CommonMonitoringCameraStore()
}
}
public extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
struct VLCItemView: View {
@ObservedObject private var store: CommonMonitoringCameraStore
private var urlString: String
private var isSelfHandling: Bool
init(store: CommonMonitoringCameraStore, urlString: String, isSelfHandling: Bool = true) {
self.store = store
self.urlString = urlString
self.isSelfHandling = isSelfHandling
}
var body: some View {
CommonMonitoringCameraView(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
if isSelfHandling { store.loadURL(urlString) }
}.onDisappear {
if isSelfHandling { store.stop() }
}
}
}
public struct CommonMonitoringCameraView: UIViewRepresentable {
@ObservedObject private var store: CommonMonitoringCameraStore
public init(store: CommonMonitoringCameraStore) {
self.store = store
}
public func makeUIView(context: Context) -> some UIView {
let uiView = store.uiView
return uiView
}
public func updateUIView(_ uiView: UIViewType, context: Context) {}
}
public class CommonMonitoringCameraStore: NSObject, ObservableObject {
var uiView: UIView = .init()
private lazy var mediaPlayer: VLCMediaPlayer = {
var player = VLCMediaPlayer()
return player
}()
private var isFirstLoad: Bool = true
private var isMediaPlayerTimeChanged: Bool = false
private var timerOpening: Timer.TimerPublisher?
private var counterOpening: Int = 0
private var cancellableSet: Set<AnyCancellable> = []
private let openingTimeout: Int = 30
public var onError = PassthroughSubject<Void, Never>()
public var onVideoStartPlaying = PassthroughSubject<Void, Never>()
@Published public var isPlaying: Bool = false
@Published public var isMute: Bool = false
@Published public var mediaPlayerState: MediaPlayerState = .unAvailable
override public init() {
super.init()
}
deinit {
stop()
}
public func loadURL(_ urlString: String) {
configMediaPlayer(urlString)
}
public func pause() {
if mediaPlayer.isPlaying {
mediaPlayer.pause()
isPlaying = false
}
}
public func play() {
mediaPlayer.play()
isPlaying = true
}
public func stop() {
mediaPlayer.stop()
isPlaying = false
}
var mediaOptions: [String] {
return [
"--network-caching=\(VLCPlayerConfig.networkCaching)",
"--rtsp-tcp",
"--rtsp-http",
"--rtsp-mcast",
"--rtsp-frame-buffer-size=\(VLCPlayerConfig.rtspFrameBufferSize)",
"--codec=avcodec",
"--avcodec-hw=none",
"--avcodec-skiploopfilter=2",
"--avcodec-skip-frame=2",
"--avcodec-skip-idct=2",
"--mms-timeout=\(VLCPlayerConfig.mmsTimeout)",
"--file-caching=3000",
"--live-caching=3000",
// "--drop-late-frames", default enable
// "--skip-frames", default enable
"--network-synchronisation",
"--video-on-top",
":clock-jitter=0",
":clock-synchro=0",
]
}
private func configMediaPlayer(_ urlString: String) {
guard !urlString.isEmpty, let url = URL(string: urlString) else {
mediaPlayerState = .error
return
}
mediaPlayer.videoAspectRatio = UnsafeMutablePointer<Int8>(mutating: (VLCPlayerConfig.aspectRatio as NSString).utf8String)
mediaPlayer.drawable = uiView
uiView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let media = VLCMedia(url: url)
mediaPlayer.media = media
mediaPlayer.delegate = self
play()
}
private func startTimerOpening() {
stopTimerOpening()
timerOpening = Timer.publish(every: 3, on: .main, in: .default)
timerOpening?.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
if counterOpening >= openingTimeout {
self.stopTimerOpening()
self.stop()
self.mediaPlayerState = .error
}
counterOpening += 3
}.store(in: &cancellableSet)
}
private func stopTimerOpening() {
counterOpening = 0
timerOpening?.autoconnect().upstream.connect().cancel()
}
}
// MARK: - VLCMediaPlayerDelegate
extension CommonMonitoringCameraStore: VLCMediaPlayerDelegate {
public func mediaPlayerStateChanged(_ aNotification: Notification) {
guard let mediaPlayer = aNotification.object as? VLCMediaPlayer else { return }
switch mediaPlayer.state {
case .opening:
mediaPlayerState = .openning
startTimerOpening()
case .playing:
mediaPlayerState = .playing
isPlaying = true
case .paused:
mediaPlayerState = .paused
isPlaying = false
case .ended:
mediaPlayerState = .ended
stopTimerOpening()
isPlaying = false
case .error:
mediaPlayerState = .error
stopTimerOpening()
isPlaying = false
default:
break
}
}
public func mediaPlayerTimeChanged(_ aNotification: Notification) {
if isFirstLoad {
onVideoStartPlaying.send(())
stopTimerOpening()
isFirstLoad = false
}
}
}
您缺少
updateUIView
和 makeCoordinator
,请将其更改为如下所示:
let url: URL
func makeCoordinator() -> MonitoringCamera {
return MonitoringCamera()
}
func makeUIView(context: Context) -> some UIView {
store.uiView
}
// called when url changes
func updateUIView(_ uiView: UIViewType, context: Context) {
if context.coordinator.loadedUrl != url {
context.coordinator.loadURL(url)
}
}