Skip to content
C.W.K.
Stream
Lesson 04 of 06 · published

The Wrist Rings the Phone Awake

~15 min · watchos, watchconnectivity, measurement, idempotency

Level 0Curious
0 XP0/80 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

Doors, and what earns one

The watch app is almost nothing but doors. The main one is Talk to Pippa: a watch-face complication opens it, and the Action Button reaches it through a published shortcut, because that is the only route watchOS allows. Dictation, a queue, a handoff to the phone.

It got smaller once. The pairing library offered a second dictation door — a keyboard-ish input screen with a globe — and it shipped as an Other language button. It came straight back from Dad's wrist:

other language 버튼 빼. 기능도 안 되고 의미 없어. swipe up으로 한영 전환 하는 거였어. — 아빠

Korean and English switch by swiping up inside the dictation screen the main door already opens. That second door was a feature duplicating a gesture, and it was removed in the next build.

A second door did arrive the next day, and it stayed. Dad wanted a way to start fresh: the default should keep talking in the existing chat, but sometimes the context needs to be reset. So under the main button sits Talk in a new chat, which sends the message with one flag set, and the phone mints a new conversation that becomes the wrist's thread from then on. The difference between the two extra doors is the whole rule: the language button did something the main door could already do; the new-chat button does the one thing the main door cannot. A door earns its place by the thing only it can do.

The 512 seconds

Here is the measurement this lesson exists for. Dad spoke a message into the watch with his phone locked in a pocket. It sat undelivered for 512 seconds — until he opened the app himself.

아이폰에서 앱을 열어두고 해야 연동이 되면 워치 앱은 사실상 쓸모가 없어져 — 아빠

The cause was a belief nobody had checked. The code used the queued-transfer door, on the assumption that iOS launches the app in the background purely to deliver one. The framework's own documentation says otherwise, in plain words: a queued transfer is delivered on next launch. Only a live send launches the counterpart app on arrival.

Two doors, two guarantees. The transfer is durable — it survives the watch being out of range, and the watch lets go of its copy only when the transfer reports landed. The live send is immediate but ephemeral. Neither one alone is what a wrist needs.

So the watch now does both. It transfers, then it rings — the same sealed envelope sent through the immediate door, right after. The transfer stays the record; the ring is what wakes the phone.

Principle: When two delivery mechanisms have different guarantees, you usually need both, and the one you are tempted to skip is the one whose guarantee you assumed. Read what the framework promises, in its own words, before building on top of what it seems to do.

Why sending twice is safe

Firing the same message through two doors would be reckless if the receiving side were not built for it. It is.

The phone's drop box writes one file per message, named by the message's id. A second arrival of the same id is the same file. Idempotent by construction, not by a check someone remembered to add. On top of that sits a seen-ledger that drops an honest echo early — but the ledger is an optimisation, not the safety. Remove it and the box is still correct.

And that id is not local trivia: it is the turn's request id. The phone dedupes on it when the watch re-offers, and the engine dedupes on it when the phone retries. One value carrying the same meaning across a watch, a phone and a server.

Write before you acknowledge

The landing has a hard ordering constraint of its own. The ring launches the phone app in the background with no interface, and the delivery callback may be the only moment that process gets — while on the other end, the watch releases its copy the moment the transfer reports landed. So the phone's copy must exist before the acknowledgement closure returns — one synchronous atomic write, on the delivery queue.

Then a separate drain moves it into the outbox from lesson 2, and the drop-box file is removed only after the outbox has it. A refusal is named and the file is kept. Same shape as the outbox's own retire ordering: the copy that can be re-read outlives the copy that cannot.

Two more things hardware taught the same afternoon

A delegate reference can be weak. The connectivity session holds its delegate weakly, so whatever stands as delegate must be retained for the life of the process. The first cut let the app's relay go out of scope — which left the phone with no delegate at all, and nothing ever arrived. Not an error, not a log line. Silence, which is the worst failure mode a message channel has.

The watch may only claim what it knows. It says Pippa is thinking… only when the phone's app actually answered the ring. Before that it says waiting for the phone while the transfer is still queued, and handed to the phone once the system has it but the app has not answered. Three different truths, three different sentences. A wrist that always says "thinking" is lying two-thirds of the time.

And the reply comes back

The last piece closed a gap Dad named immediately: the answer did not reach the wrist until the phone app was opened. The finished-turn push now carries a content-available flag, the app declares the matching background mode, and the push handler runs the drain — so the turn is found completed on the engine, marked, and the reply goes down to the watch inside the seconds iOS grants a woken app.

Where a spoken message lands is the wrist's own thread. Browsing on the phone never re-aims it; only the wrist moves that pointer. The watch is not a remote control for the phone's current screen — it is its own seat at the same table.

Code

Two doors, and why both·text
queued transfer      durable, survives out-of-range
                     delivered "on next launch"   <- waits for a human

live send            launches the counterpart app on arrival
                     ephemeral, needs reachability

First cut: transfer only.
Measured on hardware: 512 s undelivered, phone locked in a pocket.

Now: transfer (the record) THEN ring (the wake).
Whichever lands first wins — the drop box is
one file per message id, so the second is the same file.
The landing, in order·text
The ring launches the phone app in the background, no UI.
The watch releases its copy when the transfer reports landed.

  => the phone's copy must exist BEFORE the ack closure returns
     one synchronous atomic write, on the delivery queue

  drop box  --drain-->  outbox  (file removed only after the outbox has it)
  unreadable file       -> left in place, named in the status line
  refusal               -> named, file kept

The copy that can be re-read outlives the copy that cannot.

Exercise

Find one place in your system where you rely on a platform to wake, retry, or deliver something. Go read the vendor's own sentence about that guarantee — not a blog post, not your memory of it. Write down what it actually promises, then check whether your code assumes more.
Hint
'Delivered on next launch' and 'launches the app to deliver' differ by one word and by 512 seconds.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.