import Foundation import RDSpeechReaderView /// A ready-to-use PDF speech session. It owns the controller delegate so page /// navigation and temporary sentence highlighting stay synchronized. @MainActor public final class RDPDFSpeechSession: NSObject, RDSpeechReaderControllerDelegate { public let controller: RDSpeechReaderController public let contentProvider: RDPDFSpeechContentProvider public var onStateChange: ((RDSpeechReaderState) -> Void)? private weak var reader: RDPDFReaderViewController? init(reader: RDPDFReaderViewController, configuration: RDSpeechReaderConfiguration) { self.reader = reader contentProvider = reader.makeSpeechContentProvider() controller = RDSpeechReaderController(contentProvider: contentProvider, configuration: configuration) super.init() controller.delegate = self } public func start(from pageIndex: Int = 0) async throws { let location = RDSpeechLocation( bookIdentifier: contentProvider.speechBookDescriptor().identifier, resourceIdentifier: String(max(0, pageIndex)) ) try await controller.start(from: location) } public func pause() { controller.pause() } public func resume() { controller.resume() } public func stop() { controller.stop() reader?.clearSpeechHighlight() } public func speechReaderController(_ controller: RDSpeechReaderController, didChange state: RDSpeechReaderState) { if case .finished = state { reader?.clearSpeechHighlight() } if case .idle = state { reader?.clearSpeechHighlight() } onStateChange?(state) } public func speechReaderController(_ controller: RDSpeechReaderController, willSpeak range: RDSpeechSpokenRange) { guard let pageIndex = Int(range.unit.location.resourceIdentifier) else { return } reader?.showSpeechHighlight( pageIndex: pageIndex, normalizedRects: contentProvider.normalizedRects(for: range) ) } } public extension RDPDFReaderViewController { func makeSpeechSession( configuration: RDSpeechReaderConfiguration = .default ) -> RDPDFSpeechSession { RDPDFSpeechSession(reader: self, configuration: configuration) } }