#swiftui #observableobject
#swiftui #observableobject
Вопрос:
У меня есть класс playAudio для чтения аудиофайла и воспроизведения. В playAudio у меня есть функция @objc updateUI для добавления в CADisplayLink. У меня есть другое средство обновления класса, в котором я инициализирую и контролирую приостановку CADisplayLink. Я создал экземпляр @Published var playAudio: playAudio, чтобы я мог вызывать его из представления как updater.playAudio. Мой вопрос в том, что, хотя я могу печатать playAudio.positionSliderValue в режиме реального времени в активной ссылке CADisplayLink, playAudio.positionSliderValue не обновляет пользовательский интерфейс в представлении. Как я могу этого добиться? Я хочу активировать и деактивировать CADisplayLink из отдельного класса, чтобы поддерживать слабое владение (если я не ошибаюсь …). Когда @State var volume обновляется, ползунок громкости также обновляется, поэтому я думаю, что я успешно обновляю само значение, но я не могу понять, что это обновление запускает обновления в пользовательском интерфейсе. Любые мысли или предложения приветствуются. Спасибо.
import SwiftUI
import AVKit
struct ContentView: View {
@ObservedObject var updater = Updater()
@State var volume = 0.0
var body: some View {
Text("(volume)")
VStack {
Slider(value:
// in order to get continuous value changes, I do this instead of $updater.playAudio.volumeSliderValue
Binding(get: {
updater.playAudio.volumeSliderValue
}, set: { (newValue) in
updater.playAudio.volumeSliderValue = newValue
updater.playAudio.setVolume()
volume = newValue
})
, in: 0...1)
Button(action: {
updater.playAudio.play()
// activate CADisplayLink
updater.activate()
// run CADisplayLink
updater.updater?.isPaused = false
}, label: {
Text("Play File")
})
Slider(value:
// in order to get continuous value changes, I do this instead of $playAudio.positionSliderValue
Binding(get: {
updater.playAudio.positionSliderValue
}, set: { (newValue) in
updater.playAudio.positionSliderValue = newValue
updater.playAudio.seek()
})
, in: 0.0...updater.playAudio.positionSliderTotal) { _ in
updater.playAudio.seek()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Updater: ObservableObject {
var updater: CADisplayLink?
@Published var playAudio: PlayAudio
init(){
self.playAudio = PlayAudio()
self.updater = CADisplayLink(target: playAudio, selector: #selector(playAudio.updateUI))
}
func activate() {
self.updater?.add(to: .main, forMode: .default)
}
func deActivate() {
self.updater?.invalidate()
}
}
class PlayAudio: ObservableObject {
var sampleRate = Double()
var totalFrame = AVAudioFramePosition()
var startTime = AVAudioTime()
var newFramePosition = AVAudioFramePosition()
let url = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first
var audioFile = AVAudioFile()
var engine = AVAudioEngine()
var avAudioPlayerNode = AVAudioPlayerNode()
@Published var volumeSliderValue: Double = 0.7
@Published var positionSliderTotal: Double = 0.0
@Published var positionSliderValue: Double = 0.0
@objc func updateUI() {
positionSliderValue = Double(currentFrame)
// this prints ok, but I want it to update the UI in the View
print(positionSliderValue)
}
init () {
readFile()
schedulePlayer()
getTotalFrameDouble()
}
var currentFrame: AVAudioFramePosition {
guard let lastRenderTime = avAudioPlayerNode.lastRenderTime,
let playerTime = avAudioPlayerNode.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
return playerTime.sampleTime newFramePosition
}
func getTotalFrameDouble() {
positionSliderTotal = Double(totalFrame)
print(positionSliderValue)
}
func readFile() {
guard let url = url else {
return
}
do {
self.audioFile = try AVAudioFile(forReading: url)
} catch let error {
print(error)
}
self.sampleRate = audioFile.processingFormat.sampleRate
self.totalFrame = audioFile.length
}
func setupEngine() {
engine.attach(avAudioPlayerNode)
engine.connect(avAudioPlayerNode, to: engine.mainMixerNode, format: audioFile.processingFormat)
engine.prepare()
do {
try engine.start()
} catch let error {
print(error)
}
}
func schedulePlayer() {
newFramePosition = 0
engine.reset()
setupEngine()
avAudioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
}
func play() {
let outputFormat = avAudioPlayerNode.outputFormat(forBus: AVAudioNodeBus(0))
let lastRenderTime = avAudioPlayerNode.lastRenderTime?.sampleTime ?? 0
// need to convert from AVAudioFramePosition to AVAudioTime
startTime = AVAudioTime(sampleTime: AVAudioFramePosition(Double(lastRenderTime)), atRate: Double(outputFormat.sampleRate))
avAudioPlayerNode.play(at: startTime)
}
func seek() {
// player time (needs to be converted to player node time
newFramePosition = AVAudioFramePosition(positionSliderValue)
let framesToPlay = totalFrame - newFramePosition
avAudioPlayerNode.stop()
if framesToPlay > 100 {
avAudioPlayerNode.scheduleSegment(audioFile, startingFrame: newFramePosition, frameCount: AVAudioFrameCount(framesToPlay), at: nil, completionHandler: nil)
}
play()
}
func setVolume() {
avAudioPlayerNode.volume = Float(volumeSliderValue)
}
}
Комментарии:
1. ObservedObject наблюдает только за свойствами одного уровня и не имеет иерархии внутренних других ObservableObject. Вам необходимо разделить вложенные представления, зависящие от playAudio, и явно наблюдать за ними внутри этих вложенных представлений.
2. Спасибо за информацию! Я не знал, что ObservedObject наблюдает только один уровень. Я изменю его и посмотрю, как я могу улучшить.