diff --git a/TypeReader/Settings/UserDefaultsSettings.swift b/TypeReader/Settings/UserDefaultsSettings.swift index 9625119..a57dd5b 100644 --- a/TypeReader/Settings/UserDefaultsSettings.swift +++ b/TypeReader/Settings/UserDefaultsSettings.swift @@ -2,35 +2,39 @@ import Foundation import AVFoundation protocol AppSettings { - var selectedVoiceIdentifier: String { get set } + var selectedVoiceIdentifier: String? { get set } var speechRate: Float { get set } } +protocol UserSettings { + func string(forKey defaultName: String) -> String? + func float(forKey defaultName: String) -> Float + func set(_ value: Any?, forKey defaultName: String) + func set(_ value: Float, forKey defaultName: String) +} + +extension UserDefaults: UserSettings {} + class UserDefaultsSettings: AppSettings { private let userSettings: UserSettings - var selectedVoiceIdentifier: String { - didSet { - userSettings.set(selectedVoiceIdentifier, forKey: "selectedVoiceIdentifier") + var selectedVoiceIdentifier: String? { + get { + userSettings.string(forKey: "selectedVoiceIdentifier") + } + set { + userSettings.set(newValue, forKey: "selectedVoiceIdentifier") } } var speechRate: Float { - didSet { - userSettings.set(speechRate, forKey: "speechRate") + get { + userSettings.float(forKey: "speechRate") + } + set { + userSettings.set(newValue, forKey: "speechRate") } } init(userDefaults: UserSettings = UserDefaults.standard) { self.userSettings = userDefaults - if let voiceIdentifier = userSettings.string(forKey: "selectedVoiceIdentifier") { - selectedVoiceIdentifier = voiceIdentifier - } else { - selectedVoiceIdentifier = (AVSpeechSynthesisVoice.speechVoices().first { voice in - voice.language == AVSpeechSynthesisVoice.currentLanguageCode() - })?.identifier ?? "There are no voices installed" - } - self.speechRate = userSettings.float(forKey: "speechRate") - if self.speechRate == 0 { - self.speechRate = AVSpeechUtteranceDefaultSpeechRate - } } } diff --git a/TypeReader/Speaking/SpeechSynthesizer.swift b/TypeReader/Speaking/SpeechSynthesizer.swift index bd395b5..2d9b4f1 100644 --- a/TypeReader/Speaking/SpeechSynthesizer.swift +++ b/TypeReader/Speaking/SpeechSynthesizer.swift @@ -11,30 +11,37 @@ protocol SpeechSynthesizerDelegate: AnyObject { func speechSynthesizerDidPause(_: SpeechSynthesizer) } -protocol UserSettings { - func string(forKey defaultName: String) -> String? - func float(forKey defaultName: String) -> Float - func set(_ value: Any?, forKey defaultName: String) - func set(_ value: Float, forKey defaultName: String) -} - -extension UserDefaults: UserSettings {} - class SpeechSynthesizer: NSObject { static let shared = SpeechSynthesizer() private let speechSynthesizer = AVSpeechSynthesizer() var currentUtterance: AVSpeechUtterance? public weak var delegate: SpeechSynthesizerDelegate? - private let userSettings: UserSettings + private let settings: AppSettings + private var currentTitle: String? + private var currentSubtitle: String? - init(userDefaults: UserSettings = UserDefaults.standard) { - userSettings = userDefaults + init(settings: AppSettings = UserDefaultsSettings()) { + self.settings = settings super.init() speechSynthesizer.delegate = self setupAudioSession() configureRemoteTransportControls() + NotificationCenter.default.addObserver(self, selector: #selector(didChangeRate), name: .didChangeRate, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didChangeVoice), name: .didChangeVoice, object: nil) + } + + @objc + func didChangeRate(_ notification: Notification) { + guard let currentUtterance, let currentTitle, let currentSubtitle else { return } + speakText(currentUtterance.speechString, title: currentTitle, subtitle: currentSubtitle) + } + + @objc + func didChangeVoice(_ notification: Notification) { + guard let currentUtterance, let currentTitle, let currentSubtitle else { return } + speakText(currentUtterance.speechString, title: currentTitle, subtitle: currentSubtitle) } private func setupAudioSession() { @@ -76,10 +83,13 @@ class SpeechSynthesizer: NSObject { } func speakText(_ text: String, title: String, subtitle: String) { + currentTitle = title + currentSubtitle = subtitle + speechSynthesizer.stopSpeaking(at: .immediate) let utterance = AVSpeechUtterance(string: text) - if let identifier = userSettings.string(forKey: "selectedVoiceIdentifier"), + if let identifier = settings.selectedVoiceIdentifier, let voice = AVSpeechSynthesisVoice.speechVoices().first(where: { $0.identifier == identifier }) { utterance.voice = voice @@ -91,7 +101,7 @@ class SpeechSynthesizer: NSObject { let defaultVoice = AVSpeechSynthesisVoice.speechVoices().first(where: { $0.language == AVSpeechSynthesisVoice.currentLanguageCode() }) utterance.voice = voices.first ?? defaultVoice } - utterance.rate = userSettings.float(forKey: "speechRate") + utterance.rate = settings.speechRate if utterance.rate == 0 { utterance.rate = AVSpeechUtteranceDefaultSpeechRate } diff --git a/TypeReader/ViewModels/SpeechSettingsViewModel.swift b/TypeReader/ViewModels/SpeechSettingsViewModel.swift index 2f1a8a6..228e011 100644 --- a/TypeReader/ViewModels/SpeechSettingsViewModel.swift +++ b/TypeReader/ViewModels/SpeechSettingsViewModel.swift @@ -1,16 +1,47 @@ import Foundation import AVFoundation +extension NSNotification.Name { + static let didChangeRate = Notification.Name("com.onato.typereader.didChangeRate") + static let didChangeVoice = Notification.Name("com.onato.typereader.didChangeVoice") +} + +protocol Notifier { + func post(name: Notification.Name) +} +extension NotificationCenter: Notifier { + func post(name: Notification.Name) { + post(name: name, object: nil) + } +} + class SpeechSettingsViewModel: ObservableObject { - @Published var selectedVoiceIdentifier: String - - @Published var speechRate: Float + @Published var selectedVoiceIdentifier: String { + didSet { + settings.selectedVoiceIdentifier = selectedVoiceIdentifier + notifier.post(name: .didChangeVoice) + } + } + + @Published var speechRate: Float { + didSet { + settings.speechRate = speechRate + } + } var voices: [AVSpeechSynthesisVoice] = [] + let notifier: Notifier + var settings: AppSettings - init(settings: AppSettings = UserDefaultsSettings()) { - selectedVoiceIdentifier = settings.selectedVoiceIdentifier + init(settings: AppSettings = UserDefaultsSettings(), notifier: Notifier = NotificationCenter.default) { + selectedVoiceIdentifier = settings.selectedVoiceIdentifier ?? "" speechRate = settings.speechRate voices = AVSpeechSynthesisVoice.speechVoices() + self.notifier = notifier + self.settings = settings + } + + func releasedSlider() { + notifier.post(name: .didChangeRate) } } diff --git a/TypeReader/Views/SpeechSettingsView.swift b/TypeReader/Views/SpeechSettingsView.swift index 8ffc393..b7b0480 100644 --- a/TypeReader/Views/SpeechSettingsView.swift +++ b/TypeReader/Views/SpeechSettingsView.swift @@ -3,6 +3,7 @@ import SwiftUI struct SpeechSettingsView: View { @ObservedObject var viewModel = SpeechSettingsViewModel() + @State private var isSliding: Bool = false var body: some View { NavigationView { @@ -19,6 +20,8 @@ struct SpeechSettingsView: View { Text("Slow") } maximumValueLabel: { Text("Fast") + } onEditingChanged: { _ in + viewModel.releasedSlider() } } .navigationBarTitle("Speech Settings")