Zum Inhalt springen
Entwicklungstagebuch

Live-Werte, und Karten, die funktionieren, wo der Empfang aufhört

8 Min. Lesezeitvon Manuel

Echte Zahlen aufs Handgelenk bringen und der Watch dann beibringen, eine Karte ohne Cloud und ohne Empfang zu zeichnen. Samt der Bugs, die sich wehrten.

Ein Trail-Lauf mit Offline-Karten-Hintergrund und Routenspur

Als die Fundamente standen, kam der Teil, auf den es beim Laufen wirklich ankommt: echte Zahlen, live, am Handgelenk. Und dann der Teil, auf den ich mich am meisten freute, eine Karte, die weiterarbeitet, wenn der Empfang aufhört.

Ein Training, das nicht einschläft

Der Kern des Live-Erlebnisses ist eine HealthKit-Workout-Session. Diese Wahl dreht sich nicht nur ums Speichern am Ende. Eine aktive HKWorkoutSession ist das, was die App während des gesamten Laufs wach und im Vordergrund hält, damit die Watch nicht zwischen zwei Herzschlägen wegnickt. Tempo, Herzfrequenz, Distanz, Höhenmeter und Schrittfrequenz strömen herein, und ein Publisher verteilt sie an genau den Screen, den du gerade ansiehst.

Diesen Strom zum Funktionieren zu bringen, brauchte ein paar Runden. Drei Commits an einem einzigen Abend erzählen die Geschichte:

fix(phase-5): switch live snapshot pulse from Timer to Task.sleep loop fix(phase-5): start snapshot pulse before awaiting HK beginCollection fix(phase-5): TimelineView-driven clock + idempotent stop + instant dismiss

Kurzfassung: Sensoren feuern nach ihrem eigenen unregelmäßigen Takt, und mein erster Versuch nutzte einen Timer, der fröhlich weitertickte, während der Code noch darauf wartete, dass HealthKit mit dem Sammeln beginnt. Die Lösung war, die Uhr so zu treiben, wie das System es will, und das Beenden eines Trainings idempotent zu machen, damit ein Doppeltipp auf Stopp die Zustandsmaschine nicht verklemmt. Kleine, unglamouröse Korrekturen, die den Unterschied machen zwischen "funktioniert in der Demo" und "funktioniert beim echten Lauf, wenn du müde und ungeduldig bist".

Die Ortung hatte ihre eigene Lektion. Ich hatte Hintergrund-Standortaktualisierungen global aktiviert, was auf watchOS genau das Falsche ist:

fix(phase-5): never set allowsBackgroundLocationUpdates on watchOS

Die Watch braucht das während eines Trainings nicht, die Session garantiert die Vordergrundzeit ohnehin, und es zu setzen heißt nur, sich Ärger einzuhandeln. Ich verschob das Flag auf den Moment, in dem am iPhone ein Training startet, und unterdrückte es auf der Watch vollständig.

Zahlen, die du auf einen Blick liest

Auf der Live-Engine kamen die Widgets: verstrichene Zeit, Distanz, Tempo, Herzfrequenz, jeweils in mehreren Varianten und jeweils auf ihren Slot dimensioniert, sodass eine einzelne Hero-Ziffer riesig bleibt und ein dichtes Raster trotzdem sechs Werte unterbringt, ohne zu Brei zu werden. Dann die Diagramme, gebaut auf Swift Charts, mit Linien-, Flächen- und Balkenvarianten für Tempo, Herzfrequenz und Höhe, die sich beim Laufen live aus einem rollenden Puffer füllen.

Die Herzfrequenzzonen waren das erste Stück, das sich wirklich nach meinem eigenen anfühlte. Zonen werden aus deinen tatsächlichen Herzfrequenzdaten berechnet, wahlweise mit der Tanaka- oder Karvonen-Formel, nicht aus einer pauschalen Schätzung nach Alter. Ein Zonenband färbt den Screen, sodass du weißt, wo du bist, ohne eine Zahl zu lesen. (Später kam eine farbenblind-sichere Palette dazu, aber das ist Stoff für einen Feinschliff-Beitrag.)

Die Karte, die sich wehrte

Hier ist das Feature, auf das ich am stolzesten bin, und das, das mich am härtesten bekämpft hat.

Ich laufe die Trails rund um Lieboch, und Trails sind genau dort, wo der Handyempfang aufgibt. Ich wollte keine Karte, die schwarz wird, sobald du den Ort verlässt. Also funktionieren Offline-Karten so: Am iPhone wählst du eine Region, und die App rastert sie mit Apples Karten-Snapshotter in einen Stapel Kartenkacheln, packt sie in eine standardisierte MBTiles-Datei und schickt dieses Bündel über die direkte Watch-zu-Telefon-Verbindung. Keine Cloud, kein Account. Die Watch rendert die Kacheln dann selbst und zeichnet deine Route in der Volt-Akzentfarbe darüber, auch ganz ohne Empfang.

Jede Schicht davon leistete Widerstand.

Die Kachelerzeugung lässt den Snapshotter auf dem Main-Actor laufen, und der Snapshot, den er zurückgibt, lässt sich nicht sicher zwischen Threads weiterreichen, also musste das Encoding jeder Kachel zu PNG genau dort im Callback passieren, statt anderswohin geschoben zu werden. Diese Isolation unter Swifts strikten Swift-6-Regeln richtig hinzubekommen, war fummelig.

Dann die Übertragung. Die Datei-Transfer-API des Systems sagt dir nicht, wann eine Datei tatsächlich ankommt, also wusste das Telefon nie, dass sein Bündel gelandet war:

fix(wc-transfer): watch acks file receive so iPhone marks delivered without OS callback

Ich ließ die Watch eine ausdrückliche Bestätigung über den Sync-Kanal zurücksenden. Jetzt markiert das Telefon ein Bündel als zugestellt, weil die Watch es sagt, nicht weil es auf einen Callback wartet, der nie kommt.

Und dann das Karten-Widget selbst, gleich zweimal:

fix(watch-map): keep the camera centred on the user as they move fix(map): let the expanded map handle pan / zoom, stop swallowing its gestures

Das Erste ist im Nachhinein offensichtlich: Eine Karte, die deine Position zeigt, dir aber nicht folgt, ist keine große Karte. Das Zweite war heimtückischer. Per Doppeltipp die Karte in eine Vollbildansicht zu vergrößern, funktionierte, aber die vergrößerte Ansicht verschluckte dann jede Wisch- und Zoom-Geste, weil der Kartenzustand im wischbaren Screen-Stack gefangen war. Ich musste diesen Zustand herausheben, damit das Overlay tatsächlich auf deine Finger reagieren konnte.

Am meisten beigebracht hat mir der, bei dem es darum ging, was "offline" überhaupt bedeutet:

fix(maps-watch): trust WCSession.isReachable so the iPhone bridge counts as online

Auf einer echten Apple Watch meldet sich die Verbindung zum gekoppelten iPhone als "requires connection", nicht als "satisfied", was meine naive Netzwerkprüfung als offline las. Also fiel die Watch immer wieder auf Offline-Kacheln zurück, obwohl das Telefon direkt daneben war. Die Lösung war, zuerst der Watch-zu-Telefon-Erreichbarkeit zu vertrauen und den allgemeinen Netzwerkmonitor als Rückfallebene zu behandeln. Offensichtlich, sobald man es auf echter Hardware sieht. Unsichtbar im Simulator.

Was ich gelernt habe

Fast jeder Bug in diesem Abschnitt tauchte nur auf einem echten Gerät auf, bei einem echten Lauf, unter echten Bedingungen. Claude war hervorragend bei der Form des Codes und miserabel darin, vorherzusagen, wie sich eine echte Apple Watch auf einem nassen Trail mit wackeliger Verbindung verhält. Diese Arbeitsteilung, Maschine für die Struktur, ich für den Realitätscheck, ist ein Muster, das das ganze Projekt über hielt.

Als Nächstes: was passiert, wenn du auf Stopp tippst, und der Bug, den ich einen Speichervorgang vor dem Ernstfall erwischt habe.