如何从自定义委托 class 内部的回调更新视图?

How to update view from callback inside of custom delegate class?

我正在开发一个基督教应用程序,一切进展顺利,除了一件事:我无法解决如何在我的 AVSpeechSynthesizer 说完之后让标签更新其文本。

例如,祈祷文读完后,文本应再次更新为“播放”。它在所有其他已知场景(暂停工作、恢复工作、停止工作、重新启动工作等,标签相应更新)中都能正确执行此操作。

请在此处查看我的代码:

import SwiftUI
import AVFoundation

class GlobalVarsModel: ObservableObject {
    @Published var prayerAudioID: UUID?
    @Published var uttPrayerAudio = ""
    @Published var strAudioBtnImgStr = "play.fill"
    @Published var strAudioBtnText = "Play Audio"
    static let audioSession = AVAudioSession.sharedInstance()
    static var synthesizer = CustomAVSpeechSynth()
}

class CustomAVSpeechSynth: AVSpeechSynthesizer, AVSpeechSynthesizerDelegate {
    
    //NOT DESIRED OUTPUT LIST
    //@Published
    //@ObservedObject
    //@State
    
    @StateObject var gVars = GlobalVarsModel()
    
    override init() {
        super.init()
        delegate = self
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("Finished praying.")
        print(gVars.strAudioBtnText)
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
    }
}

struct TappedPrayerView: View {
    public var tappedPrayer: Prayer
    @StateObject public var gVars = GlobalVarsModel()
    @Environment(\.scenePhase) var scenePhase
    
    var body: some View {
        ScrollView {
            VStack {
                Text(tappedPrayer.strTitle).font(.title2).padding()
                HStack {
                    Spacer()
                    Button {
                        gVars.prayerAudioID = tappedPrayer.id
                        gVars.uttPrayerAudio = tappedPrayer.strText
                        
                        if (gVars.strAudioBtnText == "Play Audio") {
                            gVars.strAudioBtnImgStr = "pause.fill"
                            gVars.strAudioBtnText = "Pause Audio"
                            if (GlobalVarsModel.synthesizer.isSpeaking || GlobalVarsModel.synthesizer.isPaused) {
                                GlobalVarsModel.synthesizer.stopSpeaking(at: .immediate)
                                GlobalVarsModel.synthesizer.speak(AVSpeechUtterance(string: gVars.uttPrayerAudio))
                            } else {
                                GlobalVarsModel.synthesizer.speak(AVSpeechUtterance(string: gVars.uttPrayerAudio))
                            }
                        } else if (gVars.strAudioBtnText == "Pause Audio") {
                            GlobalVarsModel.synthesizer.pauseSpeaking(at: .immediate)
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Continue Audio"
                        } else if (gVars.strAudioBtnText == "Continue Audio") {
                            if (GlobalVarsModel.synthesizer.isPaused) {
                                GlobalVarsModel.synthesizer.continueSpeaking()
                                gVars.strAudioBtnImgStr = "pause.fill"
                                gVars.strAudioBtnText = "Pause Audio"
                            }
                        }
                    } label: {
                        Label(gVars.strAudioBtnText, systemImage: gVars.strAudioBtnImgStr).font(.title3).padding()
                    }.onAppear {
                        if ((GlobalVarsModel.synthesizer.isSpeaking || GlobalVarsModel.synthesizer.isPaused) && tappedPrayer.id != gVars.prayerAudioID) {
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Play Audio"
                        }
                    }
                    Spacer()
                    Button {
                        if (GlobalVarsModel.synthesizer.isSpeaking || GlobalVarsModel.synthesizer.isPaused) {
                            GlobalVarsModel.synthesizer.stopSpeaking(at: .immediate)
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Play Audio"
                            gVars.prayerAudioID = UUID(uuidString: String(Int.random(in: 0..<7)) + (gVars.prayerAudioID?.uuidString ?? "777"))
                        }
                    } label: {
                        Label("Restart", systemImage: "restart.circle.fill").font(.title3).padding()
                    }
                    Spacer()
                }
                Spacer()
                Text(tappedPrayer.strText).padding()
                Spacer()
            }
        }.onAppear {
            if (GlobalVarsModel.synthesizer.isPaused) {
                if (tappedPrayer.id == gVars.prayerAudioID) {
                    gVars.strAudioBtnImgStr = "play.fill"
                    gVars.strAudioBtnText = "Continue Audio"
                }
            } else if (GlobalVarsModel.synthesizer.isSpeaking) {
                if (tappedPrayer.id == gVars.prayerAudioID) {
                    gVars.strAudioBtnImgStr = "pause.fill"
                    gVars.strAudioBtnText = "Pause Audio"
                }
            } else {
                gVars.strAudioBtnImgStr = "play.fill"
                gVars.strAudioBtnText = "Play Audio"
            }
        }.onChange(of: scenePhase) { newPhase in
            if (newPhase == .active) {
            } else if (newPhase == .inactive) {
            } else if (newPhase == .background) {
            }
        }
    }
    
    struct TappedPrayerView_Previews: PreviewProvider {
        static var previews: some View {
            let defaultPrayer = Prayer(strTitle: "Default title", strText: "Default text")
            TappedPrayerView(tappedPrayer: defaultPrayer)
        }
    }
}

您的代码存在多个问题。

  1. 您正在初始化 GlobalVarsModel 两次。一次在视图中,一次在委托中。所以一个的变化不会反映在另一个。

  2. 您正在 AVSpeechSynthesizer 的子类中实现委托,因此它被封装在其中,您无法在事件发生时更新视图。

我更改了实现来解决这个问题:


class GlobalVarsViewmodel: NSObject, ObservableObject { //You need to derive from NSObject first, because `AVSpeechSynthesizer` is `objc` related
    @Published var prayerAudioID: UUID?
    @Published var uttPrayerAudio = ""
    @Published var strAudioBtnImgStr = "play.fill"
    @Published var strAudioBtnText = "Play Audio"
    let audioSession = AVAudioSession.sharedInstance()
    var synthesizer = CustomAVSpeechSynth()
    
    override init(){
        super.init()
        synthesizer.delegate = self // assign the delegate
    }
}

extension GlobalVarsViewmodel: AVSpeechSynthesizerDelegate{ // extend the viewmodel to implement the delegate
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("Finished praying.")
        strAudioBtnImgStr = "play.fill" // here assign the text and button appearance
        strAudioBtnText = "Play Audio"
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
    }
}
// I don´t think you need this anymore
class CustomAVSpeechSynth: AVSpeechSynthesizer {
    
    //NOT DESIRED OUTPUT LIST
    //@Published
    //@ObservedObject
    //@State
}

struct TappedPrayerView: View {
    var tappedPrayer: Prayer
    @StateObject private var gVars = GlobalVarsViewmodel()
    @Environment(\.scenePhase) var scenePhase
    
    var body: some View {
        ScrollView {
            VStack {
                Text(tappedPrayer.strTitle).font(.title2).padding()
                HStack {
                    Spacer()
                    Button {
                        gVars.prayerAudioID = tappedPrayer.id
                        gVars.uttPrayerAudio = tappedPrayer.strText
                        
                        if (gVars.strAudioBtnText == "Play Audio") {
                            gVars.strAudioBtnImgStr = "pause.fill"
                            gVars.strAudioBtnText = "Pause Audio"
                            if (gVars.synthesizer.isSpeaking || gVars.synthesizer.isPaused) {
                                gVars.synthesizer.stopSpeaking(at: .immediate)
                                gVars.synthesizer.speak(AVSpeechUtterance(string: gVars.uttPrayerAudio))
                            } else {
                                gVars.synthesizer.speak(AVSpeechUtterance(string: gVars.uttPrayerAudio))
                            }
                        } else if (gVars.strAudioBtnText == "Pause Audio") {
                            gVars.synthesizer.pauseSpeaking(at: .immediate)
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Continue Audio"
                        } else if (gVars.strAudioBtnText == "Continue Audio") {
                            if (gVars.synthesizer.isPaused) {
                                gVars.synthesizer.continueSpeaking()
                                gVars.strAudioBtnImgStr = "pause.fill"
                                gVars.strAudioBtnText = "Pause Audio"
                            }
                        }
                    } label: {
                        Label(gVars.strAudioBtnText, systemImage: gVars.strAudioBtnImgStr).font(.title3).padding()
                    }.onAppear {
                        if ((gVars.synthesizer.isSpeaking || gVars.synthesizer.isPaused) && tappedPrayer.id != gVars.prayerAudioID) {
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Play Audio"
                        }
                    }
                    Spacer()
                    Button {
                        if (gVars.synthesizer.isSpeaking || gVars.synthesizer.isPaused) {
                            gVars.synthesizer.stopSpeaking(at: .immediate)
                            gVars.strAudioBtnImgStr = "play.fill"
                            gVars.strAudioBtnText = "Play Audio"
                            gVars.prayerAudioID = UUID(uuidString: String(Int.random(in: 0..<7)) + (gVars.prayerAudioID?.uuidString ?? "777"))
                        }
                    } label: {
                        Label("Restart", systemImage: "restart.circle.fill").font(.title3).padding()
                    }
                    Spacer()
                }
                Spacer()
                Text(tappedPrayer.strText).padding()
                Spacer()
            }
        }.onAppear {
            if (gVars.synthesizer.isPaused) {
                if (tappedPrayer.id == gVars.prayerAudioID) {
                    gVars.strAudioBtnImgStr = "play.fill"
                    gVars.strAudioBtnText = "Continue Audio"
                }
            } else if (gVars.synthesizer.isSpeaking) {
                if (tappedPrayer.id == gVars.prayerAudioID) {
                    gVars.strAudioBtnImgStr = "pause.fill"
                    gVars.strAudioBtnText = "Pause Audio"
                }
            } else {
                gVars.strAudioBtnImgStr = "play.fill"
                gVars.strAudioBtnText = "Play Audio"
            }
        }.onChange(of: scenePhase) { newPhase in
            if (newPhase == .active) {
            } else if (newPhase == .inactive) {
            } else if (newPhase == .background) {
            }
        }
    }
    
    struct TappedPrayerView_Previews: PreviewProvider {
        static var previews: some View {
            let defaultPrayer = Prayer(strTitle: "Default title", strText: "Default text")
            TappedPrayerView(tappedPrayer: defaultPrayer)
        }
    }
}

备注:

  • 我将 GlobalVarsModel 的名称更改为 GlobalVarsViewmodel 因为它就是一个 Viewmodel。
  • 我将合成器变量更改为与实例相关而不是静态
  • AVAudioSession
  • 相同

编辑以澄清评论: 我将实现从静态更改为因为这里不需要它。您可以在此处阅读更多相关信息 -> https://www.donnywals.com/effectively-using-static-and-class-methods-and-properties/