如何从自定义委托 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)
}
}
}
您的代码存在多个问题。
您正在初始化 GlobalVarsModel
两次。一次在视图中,一次在委托中。所以一个的变化不会反映在另一个。
您正在 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/
我正在开发一个基督教应用程序,一切进展顺利,除了一件事:我无法解决如何在我的 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)
}
}
}
您的代码存在多个问题。
您正在初始化
GlobalVarsModel
两次。一次在视图中,一次在委托中。所以一个的变化不会反映在另一个。您正在
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/