Zum Inhalt springen
Entwicklungstagebuch

Auf der Stelle schwimmen, und Form lesen lernen

8 Min. Lesezeitvon Manuel

Ich habe einen Gegenstrompool im Garten, also musste die Watch lernen, ein Schwimmen zu messen, das sich nie bewegt, dazu ein Swift-6-Crash, der drei Anläufe zum Beseitigen brauchte.

Ein Pool-Schwimm-Screen auf der Apple Watch

Wir haben einen Pool im Garten. Er ist acht mal vier Meter und hat eine Gegenstromanlage, eine Pumpe, die dir eine stetige Strömung entgegendrückt, sodass du dagegen schwimmen kannst und nie eine Wand erreichst. Ich schwimme viel darin. Und er schuf ein Problem, für das keine Fitness-App gebaut war.

Ein Schwimmen, das sich nie bewegt

Denk darüber nach, wie eine Watch ein Bahnenschwimmen misst. Sie zählt deine Längen: Sie spürt, wie du dich von einer Wand abstößt, wendest und zurückkommst, und multipliziert mit der Beckenlänge, die du ihr genannt hast. Das ist das ganze Modell. Nimm jetzt die Wände weg. In einem Gegenstrompool hältst du zwanzig Minuten lang die Position gegen die Strömung. Du arbeitest hart, deine Herzfrequenz ist oben, deine Arme drehen sich, und was die Watch betrifft, bist du genau null Meter geschwommen, weil du nirgendwohin gekommen bist und nie gewendet hast.

Also wurde Bahnenschwimmen in Schichten aufgebaut. Zuerst die Grundlagen: vor dem Start die Beckenlänge erfassen, dann die Schwimm-Widgets, Längen, Schwimmtempo, Zugzahl, Zugfrequenz und SWOLF (der Effizienzwert, der deine Zeit und deine Züge für eine Länge addiert). Dann die Wassersperre, damit der Bildschirm Spritzer ignoriert und die Krone ihn danach freigibt. Alles Standard, alles nötig.

Dann der seltsame Teil, der Teil, der wegen meines Gartens existiert:

feat(watch): counter-current pool swim mode + fix live stroke collection feat(watch): count counter-current swim strokes from wrist motion feat(watch): simulated lanes, short counter-current pool stands in for a longer pool

Ein Gegenstromschwimmen hat keine Bahnen, also musste die Watch zuerst lernen, einfach Züge, Zeit und Herzfrequenz ehrlich zu zählen, ohne eine Distanz zu erfinden, die sie nicht kennen kann. Diese Züge zu zählen hieß, die Handgelenksbewegung direkt zu lesen, denn die normale Bahnenschwimm-Mechanik setzt Wenden voraus, die nie passieren.

Dann die "simulierten Bahnen", die ich unvernünftig gern mag. Du machst ein Referenzschwimmen, damit die Watch ungefähr lernt, wie weit du dich pro Zug bewegst. Danach zählt sie deine Züge, und sobald sie sich zu einer virtuellen Länge summieren, vibriert sie am Handgelenk. Dieses Vibrieren ist ein Signal: Schwimm nach vorne aus der Strömung heraus, mach eine echte Wende an der Beckenwand und stoß dich für die nächste Bahn zurück in die Strömung. Plötzlich erzeugt ein Schwimmen ohne natürliche Bahnen echte Distanz, Tempo, Längen und Splits. Es ist ein kleines Stück ehrliche Schätzung, das meinen seltsamen Gartenpool in ein richtiges Trainingswerkzeug verwandelt. Ich baute es, weil ich es wollte, der beste Grund, überhaupt etwas zu bauen.

Der Crash, der drei Anläufe brauchte

Irgendwo in der Schwimm- und Schrittfrequenz-Arbeit lebte der fieseste Bug des ganzen Projekts. Das Hinzufügen der Live-Schrittfrequenz, deine Schritte pro Minute, ließ fußbasierte Trainings immer wieder nach ein paar Sekunden abstürzen. Ich behob es zweimal. Es stürzte weiter ab. Der Commit, der es endlich beseitigte, ist das Befriedigendste, das ich den ganzen Monat schrieb:

fix(watch): mark CMPedometer handler @Sendable to stop isolation trap

Confirmed on device: foot-based workouts now finish cleanly. An on-device debugger backtrace finally pinned the real root cause [...] This is Swift 6's dynamic actor-isolation check. The CMPedometer completion handler is a plain closure written inside the @MainActor driver, so the compiler infers it @MainActor-isolated. CoreMotion invokes it on a background queue, and the runtime asserts "am I on the main actor?" at the closure's ENTRY, before any body code, and traps because we are not.

Hier ist, warum dieser so schwer war und so lehrreich. Das Motion-Framework ruft meinen Code auf einem Hintergrund-Thread zurück. Aber weil ich diesen Callback in einer Klasse geschrieben hatte, die auf dem Haupt-Thread läuft, nahm der Compiler still an, der Callback solle auch auf dem Haupt-Thread laufen, und Swift 6 fügt eine Laufzeitprüfung ein, die in dem Moment auslöst, in dem der Callback betreten wird, bevor eine einzige Zeile meines Codes läuft.

Dieser letzte Teil ist die ganze Geschichte. Meine ersten beiden Fixes räumten Code innerhalb des Callbacks um, hier ein Guard, dort ein Sprung auf den richtigen Thread. Nichts davon half, denn der Crash passierte an der Haustür, bevor irgendein Code von mir ausgeführt wurde. Wie der Commit sagt: "the resume-once latch and the GCD hop both rearranged body code that runs after the trap point." Ich räumte höflich Zimmer in einem Haus auf, das am Eingang abbrannte.

Der eigentliche Fix ist ein Wort: den Callback als @Sendable markieren, was dem Compiler sagt "das kann überall laufen, nimm nicht den Haupt-Thread an, füge diese Prüfung nicht ein". Es brauchte einen On-Device-Debugger und das Lesen eines echten Crash-Backtraces, um es zu sehen, denn es ließ sich im Simulator nie reproduzieren. Swift 6s strikte Nebenläufigkeit ist wirklich gut, sie fängt echte Bugs zur Compile-Zeit, aber wenn sie zur Laufzeit zuschlägt, tut sie es aus Gründen, deren Auflösung echtes Verständnis braucht. Das ist genau die Art Problem, bei der Claude und ich hin und her gingen, es schlug die vernünftig aussehenden Fixes vor, ich ließ sie auf einer echten Watch laufen, sie scheiterten, und erst der Geräte-Backtrace, gelesen von einem Menschen, der genug davon debuggt hat, zeigte auf die Haustür.

Meine eigene Form lesen

Das letzte Stück in diesem Kapitel ist das, das ich als jemand, der seine Fitness aus dem Februar-Stand neu aufbaut, am meisten nutze: das Trainingslast-Diagramm.

feat(history): daily CTL/ATL/TSB training-load chart with draggable cursor

Es verwandelt all deine Trainings in drei Linien: Fitness (ein langsamer 42-Tage-Durchschnitt deines Trainings), Ermüdung (ein schneller 7-Tage-Durchschnitt) und Form (die Lücke dazwischen). Jede Einheit wird mit einem herzfrequenzbasierten Modell bewertet, sodass ein harter Intervalltag mehr zählt als ein lockerer Dauerlauf. Zieh einen Cursor über das Diagramm und lies jeden Tag ab. Für mich ist es der Unterschied zwischen Raten und Wissen, ob ich mich sinnvoll aufbaue oder ein Loch grabe. Meiner Fitness-Linie zuzusehen, wie sie seit Februar steigt, langsam, ungleichmäßig, aber steigend, ist ehrlich gesagt eines der motivierenderen Dinge auf meinem Telefon.

Alles auf dem Gerät berechnet, aus den Trainings, die ohnehin in Apple Health liegen. Kein Account, kein Upload, wie bei allem anderen.

Als Nächstes, der letzte Beitrag: alles zusammenführen, die Wand aus kleinen Fixes, die niemand sieht, und eine ehrliche Abrechnung damit, wie es wirklich war, das mit einer KI zu bauen.