본문 바로가기
C.W.K.
Lesson 01 of 10 · published

워치는 5초짜리 화면이야

~14 min · on-the-wrist, watchos, design, capture, status

Level 0번들 열어본 사람
0 XP0/84 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"워치 넣어. 별거 아닌 거 같아도. 워치 쓰는 사람한테는 정말 편해."

손목은 뭘 하는 곳인가

워치 앱은 작은 폰 앱이 아니야. 한 손에 가방을 든 채로, 생각이 날아가기 전에 5초 안에 쓰는 물건이야. 그래서 가족 워치 앱엔 문이 하나야. 손목을 들어 컴플리케이션이나 동작 버튼을 누르고 말하면, 그 말은 이미 안전하게 담긴 거야. 훑어볼 목록도, 읽을 설정도, 키보드도 없어. 손목에서 타자를 치면 앱이 있는 이유가 무너지거든. 일지 앱의 첫 워치 화면은 "Speak"라고 적힌 SwiftUI 텍스트 필드를 썼는데, 진짜 워치에선 키보드가 열렸어. 라벨은 말하라고 약속하는데 화면은 치라고 한 거지. SwiftUI가 거기서 왜 마이크를 못 부르는지는 이 트랙의 받아쓰기 레슨에서 다뤄.

폰이 워치의 엔진이야

가족 엔진은 사설 네트워크에 사는데, 워치는 그 네트워크에 없고 앞으로도 들어갈 일이 없어. 폰 앱과 확장을 묶어주는 App Group도 페어링을 넘지 않아. 한 기기 안의 프로세스끼리만 나누거든. 그래서 워치는 엔진과 아예 이야기하지 않아. 담은 건 폰한테 넘기고, 폰은 그걸 다른 담기와 똑같이 다뤄서 자기 아웃박스로 집까지 보내. 이 트랙의 설계는 전부 폰을 한 번 거친다는 사실에서 나와. 워치는 자기 줄을 따로 들고 있고, 폰에 넘기는 일은 실패할 수 있는 네트워크 전송처럼 다뤄. 꺼진 폰은 받는 데 몇 시간이 걸릴 수도 있으니까, 담기엔 도착한 시각이 아니라 말한 시각을 붙여.

한눈에 믿는 화면은 과장하면 안 돼

사람은 워치를 한눈에 읽고 믿어. 그래서 낡은 정보나 지나치게 확신하는 문구는 다른 어느 화면보다 여기서 해로워. 일지의 워치 화면은 줄이 비어 있지 않으면 "N개 폰 기다리는 중", 비어 있으면 "폰에 넘겼어"라고 했어. 그러다 보니 새로 설치해서 한마디도 안 한 상태에서도 있지도 않은 전달을 알렸지. 사실은 둘이 아니라 셋이야. 뭔가 기다리는 중, 뭔가 배달됨, 아직 아무 일도 없음. 컴플리케이션에 개수를 안 두는 것도 같은 이유야. 컴플리케이션은 담을 때가 아니라 워치 일정에 맞춰 갱신되거든. 워치가 부를 때 폰이 대답해줘야 하는 이유도 여기 있어. "폰 기다리는 중"과 "폰이 받았어"는 폰이 실제로 한 말에서 나와야 해.

폰에 남는 것

설계의 대부분은 워치가 안 할 일을 정하는 거야. 워치는 엔진과 이야기하지 않으니까 엔진 주소도 PIN도 안 들고 있어. 훑어볼 이력도, 바꿀 설정도 안 보여줘. 그건 읽을 공간이 있는 폰의 몫이야. 손목에선 쓸 데가 없으니까 폰 데이터 사본도 안 둬. 남는 건 믿을 수 있을 만큼 작아. 아직 폰에 안 넘긴 말이 쌓인 줄, 상태 한 줄, 들어가는 길 하나가 전부야. 이렇게 작으니까 워치 앱은 폰 앱과 같은 TestFlight 빌드에 실려 나가고, 유지하는 데도 비용이 거의 안 들어.

Code

담기 하나가 손목에서 엔진까지 갔다가 돌아오는 길·text
  WRIST (not on the private network, never will be)
    tap the complication, or press the Action Button
      -> dictation opens (one door)
      -> the words commit to the watch's own queue            <- the capture is safe here
      -> transferUserInfo (the record) + sendMessage (the ring)

  PHONE (the watch's engine)
    one door for both deliveries, a ledger that drops the echo
      -> the phone's capture store, idempotent on the capture id
      -> the outbox drains to the engine at home when it can

  BACK TO THE WRIST
    the transfer's didFinish        -> the watch lets go of the entry
    the ring's reply                 -> "the phone has it", before didFinish arrives
상태 줄은 워치가 가진 사실로만 만들고, "아직 아무 일 없음"도 따로 떼어낸 상태로·swift
import Foundation

/// What the wrist may say, from facts it actually holds.
struct WristStatus: Equatable, Sendable {
    var waiting: Int            // every entry still in the watch's own queue (no didFinish yet)
    var handedOver: Int         // the phone answered the ring: it has these, the transfer is still settling
    var capturedThisSession: Bool
    var phoneReachable: Bool

    /// Three different facts, never two: something waits, something was delivered, nothing happened yet.
    var line: String {
        let stillOnWrist = waiting - handedOver
        if stillOnWrist > 0 {
            return phoneReachable ? "\(stillOnWrist) sending to the phone" : "\(stillOnWrist) waiting for the phone"
        }
        if capturedThisSession { return "The phone has it" }
        return "Nothing waiting"
    }
}

External links

Exercise

Spark의 손목 문 하나를 한 문장으로 적어. 이어서 워치 앱에 넣고 싶었던 다른 화면이나 컨트롤을 전부 적고, 각각 왜 폰에 남아야 하는지 써. 워치 타깃에 WristStatus를 넣고 네 가지 순간을 테스트해. 새로 설치했을 때, 폰에 닿지 않는 채로 두 번 담았을 때, 폰이 대답해준 담기가 셋일 때, 그리고 이것들이 섞였을 때야. 줄마다 소리 내 읽어보고, 워치가 아는 것보다 더 많이 말하는 줄은 고쳐.
Hint
폰이 꺼져 있고 아무것도 안 담았을 때도 참이 되는 줄이라면, 다른 경우엔 아마 과장하고 있을 거야. capturedThisSession은 앱이 켜질 때 일부러 초기화해. 어제 성공했다고 해서 지금 이 순간에 대해 알 수 있는 건 없거든.

Progress

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

댓글 0

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

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