トップ «前の日記(2024-08-09) 最新 次の日記(2024-10-10)» 編集

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

2024-09-12 [SwiftUI] Delegation

ヒレガス本『Cocoa Programming for OS X (5th Edition)』のサンプルSpeakLineをSwiftUIで実装してみた。

SpeakLine

テキスト領域に入力された文言を喋るという内容だ。今回のポイントは、マルチプラットフォームなテキスト領域の利用と、マルチプラットフォームなテキスト読み上げ機能の利用とデリゲートの仕組みをSwiftUIでどう利用するかだ。

テキスト領域は、TextFieldを利用するのが適切だが、固定した行数の領域を設定するのが難しかったため、TextEditorを利用した。ただ、TextEditorはプロンプトの機能がないので、それっぽく実装してみたが、テキストの入力が完了しないと消えないとイマイチな挙動となってしまった。

struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                TextEditor(text: $text)
                
                // prompt
                if text.isEmpty {
                    Text("Enter text to be spoken...") .foregroundColor(Color.gray)
                        .padding(.horizontal, 6)
                }
            }
            HStack {
                Spacer()
                Button("Stop") {
                    print("stop button clicked")
                }
                Button("Speak") {
                    if text.isEmpty {
                        print("string from TextEditor is empty")
                    } else {
                        print("string is \(text)")
                    }
                }
            }
        }
        .padding()
    }
}

2つのStopボタンとSpeakボタンで制御するのだが、喋っている時はSpeakボタンは押下できない。黙っている時はStopボタンは押下できないとしたのだが、SwiftUIは監視するステートに変化がないと描画が更新されないのと、テキストの読み上げ状況はAVSpeechSynthesizerDelegateで受け取るため、そのためのクラスを用意した。

final class Speaker: NSObject, AVSpeechSynthesizerDelegate, ObservableObject {
    @Published var isSpeaking: Bool = false
    private let speechSynth = AVSpeechSynthesizer()
 
    override init() {
        super.init()
        speechSynth.delegate = self
    }
 
    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        let locale = Locale(identifier:Locale.preferredLanguages[0])
        print("locale is \(locale)")
        let code = locale.languageCode!
        print("code is \(code)")
        utterance.voice = AVSpeechSynthesisVoice(language: code)
        speechSynth.speak(utterance)
    }
 
    func stop() {
        speechSynth.stopSpeaking(at: AVSpeechBoundary.immediate)
    }
    
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
        isSpeaking = true
    }
 
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        isSpeaking = false
    }
}

アプリケーションのローカライズを行っていないため、言語コードが"en-JP"とおかしなことになったため、少し工夫している。

let utterance = AVSpeechUtterance(string: text)
let locale = Locale(identifier:Locale.preferredLanguages[0])
let code = locale.languageCode!
utterance.voice = AVSpeechSynthesisVoice(language: code)

読み上げ状況に合わせてボタンの有効/無効が切り替わるようにした。

struct ContentView: View {
    @State private var text = ""
    @ObservedObject private var speaker: Speaker = .init()
 
    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                TextEditor(text: $text)
 
                // prompt
                if text.isEmpty {
                    Text("Enter text to be spoken...") .foregroundColor(Color.gray)
                        .padding(.horizontal, 6)
                }
            }
            HStack {
                Spacer()
                Button("Stop") {
                    print("stop button clicked")
                    speaker.stop()
                }.disabled(!speaker.isSpeaking)
                Button("Speak") {
                    if text.isEmpty {
                        print("string from TextEditor is empty")
                    } else {
                        print("string is \(text)")
                        speaker.speak(text)
                    }
                }.disabled(speaker.isSpeaking)
            }
        }
        .padding()
    }
}

_ 【ソースコード】

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/multiplatform/SpeakLine - GitHub

トップ «前の日記(2024-08-09) 最新 次の日記(2024-10-10)» 編集