SwiftUI: как опубликовать переменную в объекте-члене класса (другом экземпляре класса) и обновить пользовательский интерфейс в представлении

#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 наблюдает только один уровень. Я изменю его и посмотрю, как я могу улучшить.