问题
In my new SwiftUI project I have an AVPlayer for streaming music from url. Now I need to control the current time of playing track and the volume through sliders, here is the part of design:
now I can control the player with UserData final class and its @Published vars, like isPlaying:
final class UserData: ObservableObject {
// ...
@Published var player: AVPlayer? = nil
@Published var isPlaying: Bool = false
//...
func playPausePlayer(withSong song: Song, forPlaylist playlist: [Song]?) {
//...
if isPlaying {
player?.pause()
} else {
player?.play()
}
isPlaying.toggle()
}
}
glad to know if there is better decision for this part 🧐
The problem is that properties currentTime, duration I can take only from player or player?.currentItem, so I can't make slider like this:
@EnvironmentObject var userData: UserData
// ...
Slider(value: userData.player?.currentItem?.currentTime()!, in: 0...userData.player?.currentItem?.duration as! Double, step: 1)
How can I control these things?
回答1:
I didn't find any solution, so tried to do it on my own. I learned Combine framework a little, inherited AVPlayer class and signed it under the protocol ObservableObject and used KVO. May be it's not the best solution, but it works, hope somebody will give me advices for improving the code in future. Here are some code snippets:
import Foundation
import AVKit
import Combine
final class AudioPlayer: AVPlayer, ObservableObject {
@Published var currentTimeInSeconds: Double = 0.0
private var timeObserverToken: Any?
// ... some other staff
// MARK: Publishers
var currentTimeInSecondsPass: AnyPublisher<Double, Never> {
return $currentTimeInSeconds
.eraseToAnyPublisher()
}
// in init() method I add observer, which update time in seconds
override init() {
super.init()
registerObserves()
}
private func registerObserves() {
let interval = CMTime(seconds: 1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserverToken = self.addPeriodicTimeObserver(forInterval: interval, queue: .main) {
[weak self] _ in
self?.currentTimeInSeconds = self?.currentTime().seconds ?? 0.0
}
}
// func for rewind song time
func rewindTime(to seconds: Double) {
let timeCM = CMTime(seconds: seconds, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.seek(to: timeCM)
}
// sure I need to remove observer:
deinit {
if let token = timeObserverToken {
self.removeTimeObserver(token)
timeObserverToken = nil
}
}
}
// simplified slider
import SwiftUI
struct PlayerSlider: View {
@EnvironmentObject var player: AudioPlayer
@State private var currentPlayerTime: Double = 0.0
var song: Song // struct which contains the song length as Int
var body: some View {
HStack {
GeometryReader { geometry in
Slider(value: self.$currentPlayerTime, in: 0.0...Double(self.song.songLength))
.onReceive(self.player.currentTimeInSecondsPass) { _ in
// here I changed the value every second
self.currentPlayerTime = self.player.currentTimeInSeconds
}
// controlling rewind
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
let coefficient = abs(Double(self.song.songLength) / Double(geometry.size.width))
self.player.rewindTime(to: Double(value.location.x) * coefficient)
}))
}
.frame(height: 30)
}
}
}
update for VolumeView
For volume control I made new UIViewRepresentable struct:
import SwiftUI
import UIKit
import MediaPlayer
struct MPVolumeViewRepresenter: UIViewRepresentable {
func makeUIView(context: Context) -> MPVolumeView {
let volumeView = MPVolumeView()
volumeView.showsRouteButton = false // TODO: 'showsRouteButton' was deprecated in iOS 13.0: Use AVRoutePickerView instead.
if let sliderView = volumeView.subviews.first as? UISlider {
// custom design colors
sliderView.minimumTrackTintColor = UIColor(red: 0.805, green: 0.813, blue: 0.837, alpha: 1)
sliderView.thumbTintColor = UIColor(red: 0.805, green: 0.813, blue: 0.837, alpha: 1)
sliderView.maximumTrackTintColor = UIColor(red: 0.906, green: 0.91, blue: 0.929, alpha: 1)
}
return volumeView
}
func updateUIView(_ uiView: MPVolumeView, context: UIViewRepresentableContext<MPVolumeViewRepresenter>) {
// nothing here. really, nothing
}
}
// and you can use it like:
struct VolumeView: View {
var body: some View {
HStack(alignment: .center) {
Image("volumeDown")
.renderingMode(.original)
.resizable()
.frame(width: 24, height: 24)
MPVolumeViewRepresenter()
.frame(height: 24)
.offset(y: 2) // centering
Image("volumeUp")
.renderingMode(.original)
.resizable()
.frame(width: 24, height: 24)
}.padding(.horizontal)
}
}
来源:https://stackoverflow.com/questions/58779184/how-to-control-avplayer-in-swiftui