Das iPhone im Spiel halten
Eine Live Activity auf dem Sperrbildschirm, ein Renderer für drei Oberflächen und eine winzige Komplikation, die drei Anläufe brauchte, um nicht mehr kaputt auszusehen.

Dein iPhone verbringt deinen ganzen Lauf in einer Tasche oder einem Armband und tut nichts. Das fühlte sich immer nach Verschwendung an. Die Watch ist das Instrument, aber das Telefon hat den großen, hellen Bildschirm. Also geht es in diesem Kapitel darum, es an die Arbeit zu schicken, und es enthält meinen Lieblings-Bug des ganzen Projekts.
Ein Lauf auf dem Sperrbildschirm
Das Feature ist eine Live Activity: Während ein Training auf deiner Watch läuft, zeigen dein iPhone-Sperrbildschirm und die Dynamic Island ein Live-Banner mit verstrichener Zeit, Distanz, Tempo und Herzfrequenz. Ein Blick aufs Telefon, und du siehst den Lauf. Keine App zum Öffnen.
Der Mechanismus ist im Prinzip einfach. Die Watch sendet ein paarmal pro Minute einen kompakten Tick, das Telefon empfängt ihn und aktualisiert das Banner. Alles über die direkte Watch-zu-Telefon-Verbindung, nie über die Cloud, sodass das Telefon ohne Server dazwischen im Gleichschritt mit deinem Handgelenk bleibt.
Es funktionierte nicht. Das Banner tauchte zu spät auf, oder nachdem das Training schon vorbei war, oder gar nicht. Und die Commit-Nachricht, die ich schrieb, als ich endlich verstand, warum, behalte ich für immer:
fix(sync): deliver Live Activity commands over a non-coalescing channel
Live-Activity lifecycle messages were pushed over updateApplicationContext, the same coalescing, latest-wins rail used for settings. That rail collapses to a single value [...] so while the iPhone sat backgrounded the start was deferred and intermediate commands were coalesced away (sometimes the only value ever delivered was stop, so nothing showed).
Lass mich das auspacken, denn es ist ein wunderschönes Versagen. Ich schickte die Befehle "fang an, das Banner zu zeigen", "hier ist ein Update" und "stopp" über denselben Kanal, den ich für Einstellungen nutze. Dieser Kanal ist auf Effizienz ausgelegt: Wenn du ihm schnell fünf Werte schickst, stellt er sich nur die Mühe, den letzten zuzustellen, denn für eine Einstellung ist das richtig, dich interessiert nur der Endzustand. Aber für eine Folge von Live-Befehlen ist das katastrophal. Das System warf, mit dem Telefon gesperrt in deiner Tasche, hilfsbereit das "start" und alle Updates weg und stellte nur das "stopp" zu. Das Telefon stoppte pflichtbewusst ein Banner, von dem ihm nie gesagt worden war, es zu zeigen.
Die Lösung war, Live-Befehlen ihren eigenen, nicht zusammenfassenden Kanal zu geben, der jede Nachricht der Reihe nach zustellt: einen sofortigen Weg, wenn das Telefon erreichbar ist, und eine haltbare, gepufferte Rückfallebene, wenn nicht.
In dieser Commit-Nachricht steckt ein Detail, das ich liebe: "It surfaced now that the watch no longer crashes mid-workout, exposing a full workout's worth of Live-Activity traffic for the first time." Mit anderen Worten: Dieser Bug war immer da gewesen. Ich sah ihn erst, nachdem ich einen unzusammenhängenden Crash behoben hatte, denn davor überlebte die Watch nie lange genug, um den vollen Strom an Befehlen zu senden, der ihn entlarvte. Einen Bug zu beheben deckte einen anderen auf, den der erste höflich versteckt hatte. Das ist Softwareentwicklung in einem Satz.
Ein Renderer für drei Oberflächen
Ungefähr zur selben Zeit zahlte ich eine Schuld ab, die sich still angesammelt hatte. Ein konfigurierter Screen, dein eigenes Layout aus Widgets, wurde an drei verschiedenen Stellen gezeichnet: die Live-Ansicht auf der Watch, die Leerlauf-Vorschau auf der Watch und die Editor-Vorschau am iPhone. Drei Stellen, die dasselbe zeichnen, heißt drei subtil unterschiedliche Ergebnisse, und sie drifteten bereits auseinander.
Also vereinheitlichte ich sie auf einen einzigen Renderer. Eine Quelle der Wahrheit, mit dünnen Hüllen für die wenigen Dinge, die sich pro Oberfläche wirklich unterscheiden. Der Bug, der bewies, warum das wichtig war, war ein guter:
fix(design): render metric numerals at true watch size on iOS
Der iPhone-Editor zeigte Ziffern in der Vorschau etwa 1,6-mal zu groß, weil die Vorschau-Leinwand auf eine einzige Watch-Größe hartcodiert war. Du entwarfst also einen Screen am Telefon, schicktest ihn aufs Handgelenk, und die Zahlen kamen kleiner heraus, als die Vorschau versprochen hatte. Der Fix lässt die Watch ihre tatsächliche Bildschirmgröße ans Telefon melden, und das Telefon zeigt die Vorschau in genau dieser Größe. Was du entwirfst, ist jetzt das, was du bekommst, was für einen Konfigurator der ganze Sinn ist.
Die Komplikation, die drei Anläufe brauchte
Und jetzt das kleine Ding, das mich demütigte.
Eine Komplikation ist die winzige Runara-Marke, die du aufs Zifferblatt setzen kannst, um die App zu starten. Winzig. Wie schwer kann das sein. Die Antwort, dauerhaft in meiner Git-Historie an einem einzigen peinlichen Tag festgehalten:
fix(complication): use a template silhouette for the Runara mark fix(complication): shrink mark to 64px so WidgetKit archival succeeds fix(complication): crop mark to content + shrink to 48px for archival
Es stellt sich heraus, dass Komplikationen nicht wie normale Bilder rendern. Auf vielen Zifferblättern rendern sie in einem getönten Modus, der deine Farben vollständig ignoriert und die Transparenz des Bildes als Schablone nutzt, sodass ein vollfarbiges App-Symbol ohne Transparenz einfach zu einem soliden Klecks zusammenfällt. Und das System, das diese Bilder auf die Platte speichert, scheitert still, wenn sie zu groß sind, sodass meine stolze hochauflösende Marke leise abgewiesen wurde. Der Fix waren drei Schritte an einem Nachmittag: auf eine richtige Silhouetten-Vorlage umsteigen, sie auf 64 Pixel schrumpfen, feststellen, dass das immer noch nicht klein genug war, eng zuschneiden und auf 48 schrumpfen. Ein winziges Element, drei Commits, ein leicht angeschlagenes Ego.
Ich erzähle diese Geschichte mit Absicht. Die großen Features, die Live-Engine, die Offline-Karten, gingen größtenteils sauber rein. Es war das kleinste, "offensichtlichste" Element, das einen Nachmittag fraß. So oft geht die Zeit genau dorthin, und jedes ehrliche Entwicklungstagebuch muss das zugeben.
Als Nächstes: das seltsamste Feature der ganzen App, geboren aus einem Pool in meinem Garten, dazu ein Crash, der einen On-Device-Debugger und drei Anläufe zum Beseitigen brauchte.