๋ฉ์์ด์ฌ์์ฒ๋ผ iOS ์ฑ ์ค์ฟจ์์ ์ต์ข ํ๋ก์ ํธ๋ก ๋จ์ด์ฅ ์ฑ์ ๋ง๋ค๋ฉด์,
AVSpeechSynthesizer๋ฅผ ํ์ฉํ์ฌ TTS ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
ํ์ง๋ง ๊ตฌํ ๊ณผ์ ์์ ์์์น ๋ชปํ? ์ค๋ฅ๋ฅผ ๋ง๋ฌ๊ณ ์๊ฐ๋ณด๋ค ๊ณจ๋จธ๋ฆฌ๋ฅผ ์ฉ์๋ค.
์ฝ๋ ์์ผ๋ก๋ ์ ํ ๋ฌธ์ ๊ฐ ์์๋๋ฐ ๋ง์ด๋ค. (์ง์ง๋ก? ๋ฌธ์ ์๋ ๊ฑฐ ๋ง์?)
๊ทธ๋ ๊ฒ ๊ณ ๋ฏผํ๋ ์ค, ๊ฒฐ๊ตญ View Memory Graph Hierarchy๋ฅผ ํตํด ์ค๋ฅ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.
์ฒ์ ์จ๋ดค๋๋ฐ ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ผ๋ ๊ฑธ ๊นจ๋ฌ์๋ค.
์๋ฌดํผ ์ค๋์ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๊ฐ ์์ ๋ ์ฌ์ฉํ๋ฉด ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ฅผ ์๊ฐํ๊ณ ์ ํ๋ค.
๊ทธ ์ ์, AVSpeechSynthesizer๋ ์ด๋ป๊ฒ ๋์ํ๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ์.
AVSpeechSynthesizer์ ๋์ ๊ณผ์
์ฝ๋๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.

์ด ์ฝ๋๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ๋์ํ ๊น?
์ฐ์ AVSpeechSynthesizer์๋ AVSpeechUtterance๋ฅผ ๋ด์ ์ ์๋ ๊ทธ๋ฆ(Queue)์ด ์กด์ฌํ๋ค.
์ด๊ณณ์ AVSpeechUtterance ์ธ์คํด์ค๊ฐ ๋ด๊ธฐ๊ณ ,
speak ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด Queue์์ Pop๋๋ฉด์ TTS๊ฐ ์๋ํ๊ฒ ๋๋ค.
์ ๋๋ฉ์ด์ ์ผ๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.

๋ฌธ์ ์ํฉ ๐ง
๋ญ, ์ ๋ง ๋ณ ๊ฑฐ ์๋ค.
์ ์ ๋๋ฉ์ด์ ๋ง ๋ณด๋ฉด '์... ์ด๋ ๊ฒ ๋์ํ๋๊ตฌ๋...' ์ ๋๋ก ์๊ฐํ ์๋ ์๋ค.
๊ฐ๋จํ ์ ๋๋ฉ์ด์
์ฒ๋ผ ์ค์ ๋ก ์ฝ๋ ์์ผ๋ก๋ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค. (๊ฐ๋จํ ์ค ์์๋ค...)
์ฐ์ , ์ฐ๋ฆฌ ์ฑ์์์ ๋จ์ด ๋ฃ๊ธฐ ๋ชจ๋์๋ ์ธ ๊ฐ์ง ํ์ ์ด ์กด์ฌํ๋ค.

1. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(1๊ฐ)
2. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(์ฌ๋ฌ ๊ฐ)
3. ์ ์ฒด ๋จ์ด ๋ฃ๊ธฐ
1๊ฐ์ ๋จ์ด๋ง ๋ค์ ๋(1๋ฒ)๋ TTS๊ฐ ์คํ, ์ข ๋ฃ๋ ์ดํ์ ๋ค์ TTS๋ฅผ ์คํํ์ ๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์๋ค.
ํ์ง๋ง 2๋ฒ๊ณผ 3๋ฒ์ฒ๋ผ, TTS๊ฐ ์จ์ ํ ์ข ๋ฃํ๊ธฐ ์ ์ NavigationItems์ ์๋ "์ทจ์" ๋ฒํผ์ผ๋ก TTS๋ฅผ ๊ฐ์ ์ข ๋ฃํ๋ ์๊ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ด๋ ๊ฒ TTS๋ฅผ ๊ฐ์ ๋ก ์ข ๋ฃํ๊ฒ ๋๋ฉด ์ดํ์ TTS๊ฐ ์คํ๋์ง ์๋ ์น๋ช ์ ์ธ ์ค๋ฅ์๋ค.
์๋์ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ด๋ฟ์ผ๋ฉฐ TTS๊ฐ ์๋ํ์ง ์์๋ค.

์, ์ด๊ฑฐ๋ค!
๊ตฌ๊ธ๋ง์ ํด๋ด๋ ๋ด๊ฐ ์ํ๋ ๋ต๋ณ์ ์ฐพ์ ์ ์์๋ค.
๊ทธ๋ฌ๋ ์ค, ๋ฉํ ๋๊ป์ ํ์ ๋ง์์ด ๋จธ๋ฆฌ๋ฅผ ์ค์ณ ์ง๋๊ฐ๋ค.
'Xcode์์ ๋ฉ๋ชจ๋ฆฌ์ ๊ด๋ จ๋ ๋๋ฒ๊ทธ ํด์ ์ ๊ณตํ๋ค...'
์, ์ด๊ฑฐ๋ค!
ํ์์ ์์ฃผ ์ฌ์ฉํ๋ View Hierarchy์ ๊ฐ์ ๊ณณ์ ์กด์ฌํ๋ค.

์๋ฌดํผ ์ด ์น๊ตฌ๋ฅผ ํตํด์ ๋ฉ๋ชจ๋ฆฌ์ ์ด๋ค ์ธ์คํด์ค๋ค์ด ์ฌ๋ผ๊ฐ์๋ ์ง์ ํ์ธํด๋ดค๊ณ ,
๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋์ ๋ฐ์ํ์ง ์์ ๋๋ฅผ ๋น๊ตํด๋ดค๋ค.

์ผ์ชฝ ์ฌ์ง์ ์ ์์ ์ผ๋ก TTS๊ฐ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๊ณ ,
์ค๋ฅธ์ชฝ์ ์ทจ์ ๋ฒํผ์ ๋๋ฌ์ TTS๋ฅผ ๊ฐ์ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๋ค.
๋นจ๊ฐ ๋ฐ์ค ์น ์ ๋, AVSpeechUtterance๊ฐ ์์ํ๊ฒ ์ด์์๋ ๊ฒ์ ๋ชฉ๊ฒฉํ๋ค.
๊ทธ๋์ ์๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ ์ ๊ฑฐํ๋ฉด TTS๊ฐ ์ ์์ ์ผ๋ก ์คํ๋ ๊ฑฐ๋ผ๋ ํ์ ์ด ์๊ฒผ๋ค.
๊ทธ๋์, ์ด๋ป๊ฒ ์ ๊ฑฐํ ๊ฑด๋ฐ?
ARC๋ฅผ ํ์ฉํ๊ณ ์ ํ๋ค.
wordUtterance๋ฅผ nil๋ก ๋ง๋ค์ด ์ค์ผ๋ก์จ,
ARC์ ์ํด ๋ฉ๋ชจ๋ฆฌ์์ ์๋์ผ๋ก ์ ๊ฑฐ๋๋ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ๋ค.

ํ์ง๋ง ์๊น ์์์ ๋์ ์๋ฆฌ์์ ๋ดค๋ ๊ฒ์ฒ๋ผ

AVSpeechSynthesizer์์ wordUtterance์ ๋ํ ์ฐธ์กฐ๋ฅผ ๊ฐ๊ณ ์์๊ณ ,
์ด๋ก ์ธํด RC๊ฐ ๋ฐ์ํ์ฌ ARC๊ฐ ํด๋น ์ธ์คํด์ค๋ฅผ ์ ๊ฑฐํ์ง ์์๋ค.
๊ทธ๋์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ ๊ทผํ๋ค.
wordUtterance๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ด ์๋๋ผ ์ด๋ฅผ ๋ด๊ณ ์๋ AVSpeechSynthesizer ์์ฒด๋ฅผ ์ ๊ฑฐํ๋ ๋ฐฉํฅ์ผ๋ก ๋ง์ด๋ค.


๊ฒฐ๊ณผ๋ ์ฑ๊ณต์ ์ด์๋ค.
์์ ๋ฐ์ํ ๋ฌธ์ ๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํ ์ ์์๋ค! ๐
๋๋ฒ๊น ์ด ์๋์๋ค๋ฉด ํด๊ฒฐํ์ง ๋ชปํ ์ด์๋ก ๋จ์์ ๊ฒ์ด๋ค...
์์ผ๋ก๋ ์์ฃผ ํ์ฉํด์, ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํด๋ณด์! ๐ช
์ง์ ๋ถ๋ชํ๊ณ , ์ฐพ์๋ณด๋ฉด์ ์์ฑํ ๊ธ์ด๋ผ ์๋ชป๋ ์ ๋ณด๊ฐ ์์ ์ ์์ต๋๋ค.
ํผ๋๋ฐฑ์ ์ธ์ ๋ ์ง ํ์์ ๋๋ค. ๊ฐ์ฌํฉ๋๋ค. ๐
์ ์ฒด ์ฝ๋
import AVFoundation
import Foundation
enum SpeechType {
case single
case many
}
protocol TTSProtocol {
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType)
/// ์ฌ๋ฌ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordsAndMeanings(_ words: [Word], to language: String)
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋
func stopSpeaking()
}
final class SpeechSynthesizer: NSObject, ObservableObject, TTSProtocol {
// ์ฑ๊ธํค์ผ๋ก ์ ์
private var instance: AVSpeechSynthesizer? = AVSpeechSynthesizer()
// ๋จ์ด์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfWordAndMeaning = 0.3
// ์๋ฏธ์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfMeaningAndMeaning = 0.1
// ๋จ์ด์ ๋จ์ด ์ฌ์ด์ ๊ฐ๊ฒฉ (๋ค๋ฅธ ๋จ์ด๋ผ๋ฆฌ)
private let intervalOfWordAndWord = 0.5
// TODO: - ์ฌ์ฉ์๊ฐ ์ค์ ํ ์ธ์ด์ ๋ฐ๋ผ ๋์ ์ผ๋ก ๋ฑ๋๋ ์ฝ๋ ์ถ๊ฐ
private let meaningUtteranceLanguage = "ko-KR"
// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ์ ์ฅ
private var totalWordsCount: Int = 0
private var spokenWordsCount: Int = 1
// ๋จ์ด ๋ฃ๊ธฐ์ ์ฌ์ ์ฌ๋ถ๋ฅผ ํ์ธ
@Published var isPlaying = false
override init() {
super.init()
instance?.delegate = self
}
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType) {
if instance == nil {
instance = AVSpeechSynthesizer()
instance?.delegate = self
}
let wordUtterance = AVSpeechUtterance(string: word.word ?? "") // TTS๋ก ๋ค๋ ค์ค ๋จ์ด ์ค์
wordUtterance.voice = AVSpeechSynthesisVoice(language: language) // ์ธ์ด ์ค์
wordUtterance.postUtteranceDelay = intervalOfWordAndMeaning // ๋ค์ ๋จ์ด์์ ์๊ฐ ๊ฐ๊ฒฉ ์ค์
instance?.speak(wordUtterance) // TTS ์๋
if type == SpeechType.single { return } // contextMenu๋ก ์ ๊ทผํ ๊ฒฝ์ฐ
// meaning ํ์
์ด [String?]๋ผ์ ์ํํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
word.meaning?.forEach { meaning in
let meaningUtterance = AVSpeechUtterance(string: meaning)
meaningUtterance.voice = AVSpeechSynthesisVoice(language: meaningUtteranceLanguage)
meaningUtterance.postUtteranceDelay = intervalOfMeaningAndMeaning
instance?.speak(meaningUtterance)
}
}
/// ์ฌ๋ฌ ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
func speakWordsAndMeanings(_ words: [Word], to language: String) {
totalWordsCount = calculateWordsAndMeanings(words) // ๋จ์ด ๊ฐ์ ์นด์ดํธ
spokenWordsCount = 1
isPlaying = true // ์ฌ์ ์ํ๋ก ๋ณ๊ฒฝ
words.forEach { word in
self.speakWordAndMeaning(word, to: language, .many)
}
}
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋ (onDisapper)
/// TTS๊ฐ ์๋ ์ค์ธ ์ํ์์ผ๋ฉด (์ ํ ๋ฃ๊ธฐ, ์ ์ฒด ๋ฃ๊ธฐ ๋์ค ์ข
๋ฃ) AVSpeechSynthesizer๋ฅผ ์ด๊ธฐํํ์ฌ ์ค๋ฅ๋ฅผ ๋ฐฉ์ง
func stopSpeaking() {
instance?.stopSpeaking(at: .immediate)
instance = nil // ์ธ์คํด์ค ์ด๊ธฐํ
isPlaying = false // ์ ์ง ์ํ๋ก ๋ณ๊ฒฝ
}
/// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ๊ณ์ฐํ๋ ๋ฉ์๋
private func calculateWordsAndMeanings(_ words: [Word]) -> Int {
var cnt = 0
for word in words {
cnt += 1
for _ in word.meaning ?? [] {
cnt += 1
}
}
return cnt
}
}
extension SpeechSynthesizer: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
// ์ฌ์์ด ์๋ฃ๋ ๋จ์ด์ ๊ฐ์๊ฐ ์ด ๋จ์ด์ ๊ฐ์์ ์ผ์นํ๋ฉด ์ ์ง
if totalWordsCount == spokenWordsCount {
isPlaying = false
} else {
spokenWordsCount += 1
}
}
}
๊ด๊ณ
๊ธฐ๋ณธ ์ฑ์ค๋ฝ์ง๋ง(์๋ํ ๋์์ธ์
๋๋ค ใ
ใ
) ์จ๊ฒจ์ง ๊ธฐ๋ฅ์ด ๋ง์ต๋๋ค.
๋ง์ด ์ฌ์ฉํด์ฃผ์๊ณ , ์ ํฌ๊ฐ ๋ฐ๊ฒฌํ์ง ๋ชปํ ๋ฒ๊ทธ๊ฐ ์์ ๊ฒฝ์ฐ์ ์ธ์ ๋ ์ง ์ ๋ณดํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค. ๐ฝ
โTheVoca
โ## ์ฃผ์๊ธฐ๋ฅ ์์ฝ๊ฒ ๋จ์ด๋ฅผ ์ถ๊ฐํ๊ณ , ๋จ์ด์ฅ์ผ๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ์ธ์. - ๋ด๊ฐ ๊ณต๋ถํ๊ณ ์๋ ๋จ์ด์ ์๋ฏธ๋ฅผ ์ง์ ํ๋ํ๋ ๋ฑ๋กํ ์ ์์ต๋๋ค. - ๋ํ ๋ง์ ๋จ์ด๋ฅผ ํ ๋ฒ์ ๋ฑ๋กํ๊ธฐ ์ํด ์์
apps.apple.com
'๐ป ๊ฐ๋ฐ > iOS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ฉ์์ด์ฌ์์ฒ๋ผ iOS ์ฑ ์ค์ฟจ์์ ์ต์ข ํ๋ก์ ํธ๋ก ๋จ์ด์ฅ ์ฑ์ ๋ง๋ค๋ฉด์,
AVSpeechSynthesizer๋ฅผ ํ์ฉํ์ฌ TTS ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
ํ์ง๋ง ๊ตฌํ ๊ณผ์ ์์ ์์์น ๋ชปํ? ์ค๋ฅ๋ฅผ ๋ง๋ฌ๊ณ ์๊ฐ๋ณด๋ค ๊ณจ๋จธ๋ฆฌ๋ฅผ ์ฉ์๋ค.
์ฝ๋ ์์ผ๋ก๋ ์ ํ ๋ฌธ์ ๊ฐ ์์๋๋ฐ ๋ง์ด๋ค. (์ง์ง๋ก? ๋ฌธ์ ์๋ ๊ฑฐ ๋ง์?)
๊ทธ๋ ๊ฒ ๊ณ ๋ฏผํ๋ ์ค, ๊ฒฐ๊ตญ View Memory Graph Hierarchy๋ฅผ ํตํด ์ค๋ฅ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.
์ฒ์ ์จ๋ดค๋๋ฐ ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ผ๋ ๊ฑธ ๊นจ๋ฌ์๋ค.
์๋ฌดํผ ์ค๋์ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๊ฐ ์์ ๋ ์ฌ์ฉํ๋ฉด ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ฅผ ์๊ฐํ๊ณ ์ ํ๋ค.
๊ทธ ์ ์, AVSpeechSynthesizer๋ ์ด๋ป๊ฒ ๋์ํ๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ์.
AVSpeechSynthesizer์ ๋์ ๊ณผ์
์ฝ๋๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.

์ด ์ฝ๋๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ๋์ํ ๊น?
์ฐ์ AVSpeechSynthesizer์๋ AVSpeechUtterance๋ฅผ ๋ด์ ์ ์๋ ๊ทธ๋ฆ(Queue)์ด ์กด์ฌํ๋ค.
์ด๊ณณ์ AVSpeechUtterance ์ธ์คํด์ค๊ฐ ๋ด๊ธฐ๊ณ ,
speak ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด Queue์์ Pop๋๋ฉด์ TTS๊ฐ ์๋ํ๊ฒ ๋๋ค.
์ ๋๋ฉ์ด์ ์ผ๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.

๋ฌธ์ ์ํฉ ๐ง
๋ญ, ์ ๋ง ๋ณ ๊ฑฐ ์๋ค.
์ ์ ๋๋ฉ์ด์ ๋ง ๋ณด๋ฉด '์... ์ด๋ ๊ฒ ๋์ํ๋๊ตฌ๋...' ์ ๋๋ก ์๊ฐํ ์๋ ์๋ค.
๊ฐ๋จํ ์ ๋๋ฉ์ด์
์ฒ๋ผ ์ค์ ๋ก ์ฝ๋ ์์ผ๋ก๋ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค. (๊ฐ๋จํ ์ค ์์๋ค...)
์ฐ์ , ์ฐ๋ฆฌ ์ฑ์์์ ๋จ์ด ๋ฃ๊ธฐ ๋ชจ๋์๋ ์ธ ๊ฐ์ง ํ์ ์ด ์กด์ฌํ๋ค.

1. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(1๊ฐ)
2. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(์ฌ๋ฌ ๊ฐ)
3. ์ ์ฒด ๋จ์ด ๋ฃ๊ธฐ
1๊ฐ์ ๋จ์ด๋ง ๋ค์ ๋(1๋ฒ)๋ TTS๊ฐ ์คํ, ์ข ๋ฃ๋ ์ดํ์ ๋ค์ TTS๋ฅผ ์คํํ์ ๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์๋ค.
ํ์ง๋ง 2๋ฒ๊ณผ 3๋ฒ์ฒ๋ผ, TTS๊ฐ ์จ์ ํ ์ข ๋ฃํ๊ธฐ ์ ์ NavigationItems์ ์๋ "์ทจ์" ๋ฒํผ์ผ๋ก TTS๋ฅผ ๊ฐ์ ์ข ๋ฃํ๋ ์๊ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ด๋ ๊ฒ TTS๋ฅผ ๊ฐ์ ๋ก ์ข ๋ฃํ๊ฒ ๋๋ฉด ์ดํ์ TTS๊ฐ ์คํ๋์ง ์๋ ์น๋ช ์ ์ธ ์ค๋ฅ์๋ค.
์๋์ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ด๋ฟ์ผ๋ฉฐ TTS๊ฐ ์๋ํ์ง ์์๋ค.

์, ์ด๊ฑฐ๋ค!
๊ตฌ๊ธ๋ง์ ํด๋ด๋ ๋ด๊ฐ ์ํ๋ ๋ต๋ณ์ ์ฐพ์ ์ ์์๋ค.
๊ทธ๋ฌ๋ ์ค, ๋ฉํ ๋๊ป์ ํ์ ๋ง์์ด ๋จธ๋ฆฌ๋ฅผ ์ค์ณ ์ง๋๊ฐ๋ค.
'Xcode์์ ๋ฉ๋ชจ๋ฆฌ์ ๊ด๋ จ๋ ๋๋ฒ๊ทธ ํด์ ์ ๊ณตํ๋ค...'
์, ์ด๊ฑฐ๋ค!
ํ์์ ์์ฃผ ์ฌ์ฉํ๋ View Hierarchy์ ๊ฐ์ ๊ณณ์ ์กด์ฌํ๋ค.

์๋ฌดํผ ์ด ์น๊ตฌ๋ฅผ ํตํด์ ๋ฉ๋ชจ๋ฆฌ์ ์ด๋ค ์ธ์คํด์ค๋ค์ด ์ฌ๋ผ๊ฐ์๋ ์ง์ ํ์ธํด๋ดค๊ณ ,
๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋์ ๋ฐ์ํ์ง ์์ ๋๋ฅผ ๋น๊ตํด๋ดค๋ค.

์ผ์ชฝ ์ฌ์ง์ ์ ์์ ์ผ๋ก TTS๊ฐ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๊ณ ,
์ค๋ฅธ์ชฝ์ ์ทจ์ ๋ฒํผ์ ๋๋ฌ์ TTS๋ฅผ ๊ฐ์ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๋ค.
๋นจ๊ฐ ๋ฐ์ค ์น ์ ๋, AVSpeechUtterance๊ฐ ์์ํ๊ฒ ์ด์์๋ ๊ฒ์ ๋ชฉ๊ฒฉํ๋ค.
๊ทธ๋์ ์๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ ์ ๊ฑฐํ๋ฉด TTS๊ฐ ์ ์์ ์ผ๋ก ์คํ๋ ๊ฑฐ๋ผ๋ ํ์ ์ด ์๊ฒผ๋ค.
๊ทธ๋์, ์ด๋ป๊ฒ ์ ๊ฑฐํ ๊ฑด๋ฐ?
ARC๋ฅผ ํ์ฉํ๊ณ ์ ํ๋ค.
wordUtterance๋ฅผ nil๋ก ๋ง๋ค์ด ์ค์ผ๋ก์จ,
ARC์ ์ํด ๋ฉ๋ชจ๋ฆฌ์์ ์๋์ผ๋ก ์ ๊ฑฐ๋๋ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ๋ค.

ํ์ง๋ง ์๊น ์์์ ๋์ ์๋ฆฌ์์ ๋ดค๋ ๊ฒ์ฒ๋ผ

AVSpeechSynthesizer์์ wordUtterance์ ๋ํ ์ฐธ์กฐ๋ฅผ ๊ฐ๊ณ ์์๊ณ ,
์ด๋ก ์ธํด RC๊ฐ ๋ฐ์ํ์ฌ ARC๊ฐ ํด๋น ์ธ์คํด์ค๋ฅผ ์ ๊ฑฐํ์ง ์์๋ค.
๊ทธ๋์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ ๊ทผํ๋ค.
wordUtterance๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ด ์๋๋ผ ์ด๋ฅผ ๋ด๊ณ ์๋ AVSpeechSynthesizer ์์ฒด๋ฅผ ์ ๊ฑฐํ๋ ๋ฐฉํฅ์ผ๋ก ๋ง์ด๋ค.


๊ฒฐ๊ณผ๋ ์ฑ๊ณต์ ์ด์๋ค.
์์ ๋ฐ์ํ ๋ฌธ์ ๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํ ์ ์์๋ค! ๐
๋๋ฒ๊น ์ด ์๋์๋ค๋ฉด ํด๊ฒฐํ์ง ๋ชปํ ์ด์๋ก ๋จ์์ ๊ฒ์ด๋ค...
์์ผ๋ก๋ ์์ฃผ ํ์ฉํด์, ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํด๋ณด์! ๐ช
์ง์ ๋ถ๋ชํ๊ณ , ์ฐพ์๋ณด๋ฉด์ ์์ฑํ ๊ธ์ด๋ผ ์๋ชป๋ ์ ๋ณด๊ฐ ์์ ์ ์์ต๋๋ค.
ํผ๋๋ฐฑ์ ์ธ์ ๋ ์ง ํ์์ ๋๋ค. ๊ฐ์ฌํฉ๋๋ค. ๐
์ ์ฒด ์ฝ๋
import AVFoundation
import Foundation
enum SpeechType {
case single
case many
}
protocol TTSProtocol {
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType)
/// ์ฌ๋ฌ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordsAndMeanings(_ words: [Word], to language: String)
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋
func stopSpeaking()
}
final class SpeechSynthesizer: NSObject, ObservableObject, TTSProtocol {
// ์ฑ๊ธํค์ผ๋ก ์ ์
private var instance: AVSpeechSynthesizer? = AVSpeechSynthesizer()
// ๋จ์ด์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfWordAndMeaning = 0.3
// ์๋ฏธ์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfMeaningAndMeaning = 0.1
// ๋จ์ด์ ๋จ์ด ์ฌ์ด์ ๊ฐ๊ฒฉ (๋ค๋ฅธ ๋จ์ด๋ผ๋ฆฌ)
private let intervalOfWordAndWord = 0.5
// TODO: - ์ฌ์ฉ์๊ฐ ์ค์ ํ ์ธ์ด์ ๋ฐ๋ผ ๋์ ์ผ๋ก ๋ฑ๋๋ ์ฝ๋ ์ถ๊ฐ
private let meaningUtteranceLanguage = "ko-KR"
// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ์ ์ฅ
private var totalWordsCount: Int = 0
private var spokenWordsCount: Int = 1
// ๋จ์ด ๋ฃ๊ธฐ์ ์ฌ์ ์ฌ๋ถ๋ฅผ ํ์ธ
@Published var isPlaying = false
override init() {
super.init()
instance?.delegate = self
}
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType) {
if instance == nil {
instance = AVSpeechSynthesizer()
instance?.delegate = self
}
let wordUtterance = AVSpeechUtterance(string: word.word ?? "") // TTS๋ก ๋ค๋ ค์ค ๋จ์ด ์ค์
wordUtterance.voice = AVSpeechSynthesisVoice(language: language) // ์ธ์ด ์ค์
wordUtterance.postUtteranceDelay = intervalOfWordAndMeaning // ๋ค์ ๋จ์ด์์ ์๊ฐ ๊ฐ๊ฒฉ ์ค์
instance?.speak(wordUtterance) // TTS ์๋
if type == SpeechType.single { return } // contextMenu๋ก ์ ๊ทผํ ๊ฒฝ์ฐ
// meaning ํ์
์ด [String?]๋ผ์ ์ํํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
word.meaning?.forEach { meaning in
let meaningUtterance = AVSpeechUtterance(string: meaning)
meaningUtterance.voice = AVSpeechSynthesisVoice(language: meaningUtteranceLanguage)
meaningUtterance.postUtteranceDelay = intervalOfMeaningAndMeaning
instance?.speak(meaningUtterance)
}
}
/// ์ฌ๋ฌ ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
func speakWordsAndMeanings(_ words: [Word], to language: String) {
totalWordsCount = calculateWordsAndMeanings(words) // ๋จ์ด ๊ฐ์ ์นด์ดํธ
spokenWordsCount = 1
isPlaying = true // ์ฌ์ ์ํ๋ก ๋ณ๊ฒฝ
words.forEach { word in
self.speakWordAndMeaning(word, to: language, .many)
}
}
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋ (onDisapper)
/// TTS๊ฐ ์๋ ์ค์ธ ์ํ์์ผ๋ฉด (์ ํ ๋ฃ๊ธฐ, ์ ์ฒด ๋ฃ๊ธฐ ๋์ค ์ข
๋ฃ) AVSpeechSynthesizer๋ฅผ ์ด๊ธฐํํ์ฌ ์ค๋ฅ๋ฅผ ๋ฐฉ์ง
func stopSpeaking() {
instance?.stopSpeaking(at: .immediate)
instance = nil // ์ธ์คํด์ค ์ด๊ธฐํ
isPlaying = false // ์ ์ง ์ํ๋ก ๋ณ๊ฒฝ
}
/// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ๊ณ์ฐํ๋ ๋ฉ์๋
private func calculateWordsAndMeanings(_ words: [Word]) -> Int {
var cnt = 0
for word in words {
cnt += 1
for _ in word.meaning ?? [] {
cnt += 1
}
}
return cnt
}
}
extension SpeechSynthesizer: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
// ์ฌ์์ด ์๋ฃ๋ ๋จ์ด์ ๊ฐ์๊ฐ ์ด ๋จ์ด์ ๊ฐ์์ ์ผ์นํ๋ฉด ์ ์ง
if totalWordsCount == spokenWordsCount {
isPlaying = false
} else {
spokenWordsCount += 1
}
}
}
๊ด๊ณ
๊ธฐ๋ณธ ์ฑ์ค๋ฝ์ง๋ง(์๋ํ ๋์์ธ์
๋๋ค ใ
ใ
) ์จ๊ฒจ์ง ๊ธฐ๋ฅ์ด ๋ง์ต๋๋ค.
๋ง์ด ์ฌ์ฉํด์ฃผ์๊ณ , ์ ํฌ๊ฐ ๋ฐ๊ฒฌํ์ง ๋ชปํ ๋ฒ๊ทธ๊ฐ ์์ ๊ฒฝ์ฐ์ ์ธ์ ๋ ์ง ์ ๋ณดํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค. ๐ฝ
โTheVoca
โ## ์ฃผ์๊ธฐ๋ฅ ์์ฝ๊ฒ ๋จ์ด๋ฅผ ์ถ๊ฐํ๊ณ , ๋จ์ด์ฅ์ผ๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ์ธ์. - ๋ด๊ฐ ๊ณต๋ถํ๊ณ ์๋ ๋จ์ด์ ์๋ฏธ๋ฅผ ์ง์ ํ๋ํ๋ ๋ฑ๋กํ ์ ์์ต๋๋ค. - ๋ํ ๋ง์ ๋จ์ด๋ฅผ ํ ๋ฒ์ ๋ฑ๋กํ๊ธฐ ์ํด ์์
apps.apple.com