Construcción de un Reproductor de Video Modular con SwiftUI y AVFoundation
La integración de la potencia declarativa de SwiftUI con las capacidades multimedia robustas de AVFoundation permite a los desarrolladores de iOS crear componentes de reproducción de video modernos y eficientes. A diferencia de los enfoques tradicionales basados en UIKit, donde la gestión del estado de la vista y la lógica de reproducción a menudo se entrelazan manualmente, el pardaigma impulsado por datos de SwiftUI facilita la construcción de reproductores más limpios y fáciles de mantener. Este artículo te guiará a través de los pasos esenciales para implementar un componente de control de video completo y reutilizable.
- Diseño del Controlador Central de Reproducción
El núcleo de cualquier reproductor de video reside en su gestión de estados. En SwiftUI, los tipos ObservableObject junto con los @Published property wrappers son ideales para crear una máquina de estados reactiva que refleje el estado actual del reproductor multimedia.
import Foundation
import AVFoundation
import Combine
import SwiftUI // Necesario para ViewModifier, etc.
class MediaPlaybackManager: ObservableObject {
@Published var avPlayer: AVPlayer?
@Published var playbackActive: Bool = false
@Published var currentPlaybackTime: CMTime = .zero
@Published var totalMediaDuration: CMTime = .zero
@Published var userIsSeeking: Bool = false
private var playerItemObserver: NSKeyValueObservation?
private var timeMonitoringObserver: Any?
init(videoURL: URL) {
let playerItem = AVPlayerItem(url: videoURL)
self.avPlayer = AVPlayer(playerItem: playerItem)
setupPlayerObservers(item: playerItem)
setupTimeMonitoring()
}
private func setupPlayerObservers(item: AVPlayerItem) {
playerItemObserver = item.observe(\.duration, options: [.new]) { [weak self] observedItem, change in
guard let newDuration = change.newValue, newDuration.isValid else { return }
self?.totalMediaDuration = newDuration
}
}
private func setupTimeMonitoring() {
let updateInterval = CMTime(seconds: 0.75, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeMonitoringObserver = avPlayer?.addPeriodicTimeObserver(
forInterval: updateInterval,
queue: .main) { [weak self] time in
guard let self = self, !self.userIsSeeking else { return }
self.currentPlaybackTime = time
}
}
func togglePlaybackState() {
if playbackActive {
avPlayer?.pause()
} else {
avPlayer?.play()
}
playbackActive.toggle()
}
func seekToTime(targetTime: CMTime) {
userIsSeeking = true
avPlayer?.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
self?.userIsSeeking = false
}
}
deinit {
if let observer = timeMonitoringObserver {
avPlayer?.removeTimeObserver(observer)
}
playerItemObserver?.invalidate()
}
}
Aspectos clave de este diseño:
- Los atributos
@Publishedgarantizan que cualquier cambio en estas propiedades notifique a la interfaz de usuario para una actualización automática. addPeriodicTimeObserversincroniza el progreso de la reproducción con la interfaz de usuario a intervalos regulares.- Se utiliza un estado
userIsSeekingindependiente para evitar que la interfaz de usuario parpadee o se actualice erróneamente durante las operaciones de búsqueda. - Los observadores de tiempo y KVO se invalidan y eliminan en
deinitpara prevenir fugas de memoria. - La duración del elemento de reproducción se obtiene a través de KVO en el
AVPlayerItem.
- Formateo de Tiempo de CMTime a String para la Interfaz
AVFoundation utiliza la estructura CMTime para representar el tiempo, pero la interfaz de usuario requiere una representación de cadena formateada. Podemos crear un ViewModifier reutilizable para este propósito.
import SwiftUI
import AVFoundation
struct TimeTextFormatterModifier: ViewModifier {
let timeValue: CMTime
private var formattedTimeString: String {
guard !timeValue.isIndefinite && !timeValue.isNull && timeValue.seconds >= 0 else {
return "--:--" // Muestra un formato predeterminado para tiempos no válidos
}
let totalSeconds = Int(timeValue.seconds)
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
func body(content: Content) -> some View {
content
.overlay(
Text(formattedTimeString)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.black.opacity(0.7))
.cornerRadius(5)
)
}
}
// Extensión para aplicar el modificador fácilmente
extension View {
func displayFormattedTime(time: CMTime) -> some View {
self.modifier(TimeTextFormatterModifier(timeValue: time))
}
}