본문 바로가기
C.W.K.
Stream
Lesson 05 of 07 · published

손목 받아쓰기: 빈 목록 말고 nil, 그리고 위로 쓸기

~16 min · on-the-wrist, dictation, watchkit, korean-input, verification

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"swipe up으로 한영 전환 하는 거였어."

여기선 SwiftUI가 마이크를 못 열어

"Speak"라고 적힌 SwiftUI 텍스트 필드를 쓴 워치 담기 화면이 진짜 워치에서 키보드를 열었어. 설정 문제도, 워치가 마지막 모드를 기억한 것도 아니었어. SwiftUI 받아쓰기 컨트롤은 watchOS에서 쓸 수 없다고 선언돼 있어. 그래서 텍스트 필드엔 시스템이 기본으로 고른 입력 화면이 뜨고, 앱은 그걸 정할 수 없어. 워치엔 Speech 프레임워크도 없어서, 앱이 고른 언어로 자기 인식기를 돌릴 수도 없어. 되는 길은 WatchKit의 presentTextInputController(withSuggestions:allowedInputMode:completion:)이고, WKApplication.shared().visibleInterfaceController를 거쳐서 부를 수 있어. 이 레슨을 쓰면서 설치된 SDK로 직접 확인했어. SwiftUI 타입엔 @available(watchOS, unavailable)이 붙어 있고, watchOS SDK엔 Speech 프레임워크가 없어.

nil과 []는 다른 호출이야

화면은 suggestions 인자가 정해. nil을 넘기면 목록이 없다는 뜻이라 watchOS가 입력 화면을 건너뛰고 바로 마이크를 열어. []를 넘기면 목록은 있는데 마침 비어 있는 거라서, watchOS가 키보드, 손글씨, 마이크 중에 고르는 입력 화면을 보여줘. 호출하는 코드만 봐선 차이가 안 보이지만, 5초 만에 끝나는 담기냐 한 번 돌아가야 하는 담기냐가 여기서 갈려. 시뮬레이터는 이걸 판정 못 해. 워치 시뮬레이터는 어느 쪽이든 키보드로 넘어가니까, 거기서 찍은 키보드 스크린샷은 아무것도 증명 못 해.

앱 셋이 출시했다가 도로 뺀 두 번째 문

이중 언어 사용자가 영어로 열린 받아쓰기에 한국어로 말했더니 엉터리가 나왔어. 언어 전환 방법을 찾던 세션은 SDK 헤더를 읽다가 사용자가 입력 언어를 바꿀 수 있는 변형을 발견했어. 그러고는 앱 안에서 언어를 바꾸는 유일한 길이 지구본이 있는 빈 목록 화면이라고 결론 내고, 버튼 둘을 출시했지. 빠른 버튼 하나, "다른 언어" 버튼 하나. 다른 앱 둘도 그 버튼 짝을 그대로 따라 했어. 정작 주인 손목에선 두 번째 버튼이 쓸모가 없었고, 진짜 답은 주인이 직접 찾았어. 평범한 받아쓰기 화면에서 위로 쓸어올리면 언어 선택이 나와. 그 버튼은 앱 셋에서 모두 빠졌고, 공유 받아쓰기 모듈의 입구도 이제 하나야.

여기서 규칙 둘이 나왔어. 첫째, 어떤 기능에 닿는 길이 이것뿐이라는 말은 플랫폼 전체에 대한 주장이야. 길 하나를 되게 만들었다고 해서 그 주장이 서진 않아. 누군가는 평범한 길도 직접 해봐야 해. 둘째, 시뮬레이터에서 찍은 버튼 스크린샷은 뷰가 그려졌다는 증거일 뿐이고, 그 버튼이 여는 시스템 시트에 대해선 아무것도 증명하지 못해. 내용을 다시 확인하다가 바로잡은 것도 하나 있어. 언어를 고르는 변형은 한때 Swift에서 못 쓴다고 기록됐는데, 지금 SDK에선 import되고 컴파일돼. 그래도 그 변형은 바로 받아쓰기로 가지 않으니까, 가족 앱의 답은 여전히 문 하나와 위로 쓸기야.

발목 잡는 디테일 둘

visibleInterfaceController는 뷰가 화면에 뜨기 전까진 nil이야. 꺼져 있던 앱을 동작 버튼으로 켜면 바로 그 틈에 호출이 떨어질 수 있어. 그러니까 "받아쓰기 못 씀"은 계속되는 상태가 아니라 그 순간의 사정이야. 세션 내내 붙들고 있지 말고 시도할 때마다 다시 확인해. 또 완료 클로저는 오디오 탭 때와 같은 이유로 nonisolated 팩토리에서 만들어. 메인 액터 메서드 안에서 쓴 클로저는 그 격리를 물려받는데, 프레임워크는 그 클로저를 다른 곳에서 부를 수 있거든.

Code

받아쓰기 문 하나: nil suggestions, nonisolated 완료 클로저, "못 씀"은 붙들어두지 않기·swift
import WatchKit

/// Dictation on watchOS goes through WatchKit. SwiftUI's dictation controls are unavailable here,
/// and there is no Speech framework on the watch.
enum WristDictation {
    /// nil suggestions opens the microphone at once. An EMPTY array would show the input list instead.
    /// The language switch is a swipe up on the dictation screen itself; the app adds no second door.
    @MainActor
    static func speak(_ deliver: @escaping @MainActor @Sendable (String?) -> Void) -> Bool {
        // nil until a view is on screen: a cold launch from the Action Button can land here too early.
        guard let controller = WKApplication.shared().visibleInterfaceController else { return false }
        controller.presentTextInputController(withSuggestions: nil, allowedInputMode: .plain,
                                              completion: completion(deliver))
        return true
    }

    /// Built off any actor. An inline closure written in a @MainActor method would inherit that
    /// isolation, and WatchKit may call it elsewhere.
    nonisolated static func completion(_ deliver: @escaping @MainActor @Sendable (String?) -> Void)
        -> @Sendable ([Any]?) -> Void {
        { results in
            let text = (results?.first as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
            Task { @MainActor in deliver(text?.isEmpty == false ? text : nil) }
        }
    }
}
기억 말고 SDK를 읽어·bash
# What the watchOS SDK actually offers, checked against the installed SDK rather than memory.
WSDK=$(xcrun --sdk watchsimulator --show-sdk-path)

# SwiftUI's dictation controls: declared unavailable on watchOS.
grep -h -B3 'struct TextInputDictationBehavior' \
  "$WSDK"/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/*.swiftinterface | head -4
#   @available(watchOS, unavailable)

# No Speech framework on the watch, so no recognizer with an app-chosen locale.
ls "$WSDK/System/Library/Frameworks" | grep -c '^Speech.framework'          # 0

# The language-choosing variant exists and imports into Swift, but its header says it
# "will never go straight to dictation".
grep -o 'will never go straight to dictation[^;]*' \
  "$WSDK/System/Library/Frameworks/WatchKit.framework/Headers/WKInterfaceController.h"

External links

Exercise

위의 SDK 확인 명령을 돌려서 결과를 하나씩 적어. 그 뒤에 Spark 워치 앱의 버튼 하나에 WristDictation.speak를 연결해. 진짜 워치에서 suggestions 인자로 nil[]를 넘겨 보고, 각각 어떤 화면이 열리는지 적어. nil로 연 받아쓰기 화면에서 위로 쓸어서 언어를 바꿔봐. 마지막으로 앱이 꺼진 상태에서 컴플리케이션으로 켜 보고, 첫 호출이 false를 돌려주면 뷰가 뜬 뒤에 다시 시도되는지 확인해.
Hint
시뮬레이터밖에 없다면 거기선 두 변형 다 키보드를 보여준다고 적고, 비교는 미결로 표시해. 재시도는 대기 중인 요청이 있을 때 뷰의 onAppear에서 speak를 한 번 더 부르면 돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.