Apps mit Skip.dev entwickeln
Hinweis: Skip ist inzwischen vollständig Open Source und kostenlos nutzbar. Vermutlich hat es sich seit Veröffentlichung dieses Beitrags auch deutlich verbessert; der folgende Text gibt meine Eindrücke zum Zeitpunkt des Schreibens wieder.
Die Entwicklung meines ersten Skip-Projekts, das gleichzeitig mein erstes SwiftUI-Projekt war, habe ich inzwischen größtenteils abgeschlossen. Deshalb möchte ich meine Erfahrungen mit Skip aus der Perspektive eines SwiftUI-Einsteigers teilen.
Das ist nicht meine erste App. Eine andere App habe ich bereits mit Ionic/Vue + Capacitor entwickelt: youProof, digitale Visitenkarten mit Ende-zu-Ende-Verschlüsselung. Dort habe ich ein wenig nativen Code für ein einfaches QR-Code-Widget verwendet, darüber hinaus habe ich aber keinerlei Erfahrung mit Swift/SwiftUI oder Kotlin/Jetpack Compose.
Ein wenig Kontext
Meine neue App heißt Vector Bible (siehe Link in meiner Bio). Sie ist ein Bibelreader mit semantischer Suche und ziemlich ordentlichem UI/UX – zumindest meiner Meinung nach. Zunächst begann sie als Web-App, aber recht schnell wurde mir klar, dass eine zusätzliche mobile App der richtige Weg wäre.
Wenn ich Software entwickle, versuche ich, sie mit möglichst vielen Geräten kompatibel zu machen – allerdings nur, soweit das für einen Solo-Entwickler noch vernünftig bleibt. Dasselbe gilt dafür, dass sich die App auf jeder Plattform nativ anfühlen soll. Bei der Entwicklung von youProof hat mir die Ionic-Developer-Experience aber nicht besonders gefallen. Es fühlte sich an, als müsste ich ständig gegen die vom Framework vorgegebenen Styles kämpfen, um genau den gewünschten Look zu erreichen. Außerdem wirken Ionic-Apps seit MD3 (Expressive) und jetzt Liquid Glass auf allen Plattformen einfach veraltet. Deshalb war komplett nativ zu entwickeln für mich der naheliegende nächste Versuch.
Als Solo-Entwickler, der Projekte in seiner Freizeit erstellt, versuche ich trotzdem immer, den Entwicklungsaufwand überschaubar zu halten, damit ich die begonnenen Projekte auch fertigstellen kann. Von Anfang an war klar, dass diese App auf iPhone, iPad und Android-Geräten laufen sollte. Etwa zur gleichen Zeit sah ich zufällig einige Posts auf X über Skip – einen Transpiler von Swift/SwiftUI zu Kotlin/Jetpack Compose. Das klang interessant, also beschloss ich, es auszuprobieren.
Skip kurz erklärt
Aber was ist Skip eigentlich? Skip ist im Grunde ein Toolkit, das eine SwiftUI-App automatisch in eine Compose-App umwandelt. Dafür stellt es Compose-Ersatz für übliche SwiftUI-Komponenten bereit. Swift-Code wird zu Kotlin transpiliert und SwiftUI-Komponenten werden durch entsprechende Compose-Komponenten ersetzt. Inzwischen gibt es auch einen Compile-Modus, in dem Swift-Code nicht mehr transpiliert, sondern tatsächlich für Android-Ziele kompiliert wird. Der ist allerdings nur für zahlende Nutzer verfügbar, daher habe ich ihn nicht verwendet.
Die Grundidee ist, die App von Beginn an mit Skip zu entwickeln. Man erstellt das Projekt mit Skip und testet während der Entwicklung laufend auf iOS und Android. Ich empfehle, jederzeit ein physisches Android- und iOS-Gerät zur Hand zu haben. Das ist bequemer, als zusätzlich zu Xcode und Android Studio zwei virtuelle Geräte laufen zu lassen – beide IDEs braucht man für die Entwicklung mit Skip ohnehin.
Skips Ziel ist, dass man eine iOS-App mit iOS-Werkzeugen entwickelt und die Android-App gratis dazubekommt. Natürlich kann Skip aber nicht sinnvoll die gesamte SwiftUI-API unterstützen. Deshalb unterstützt es nur einen Teil von SwiftUI und konzentriert sich auf wichtigere Bereiche der API. Genau deshalb ist es nicht optimal oder empfohlen, Skip zu einer bestehenden SwiftUI-App hinzuzufügen.
Meine Erfahrung
Alles in allem funktioniert das ziemlich gut. Solange man der größtenteils brauchbaren Dokumentation folgt, erhält man eine App, die auf dem iPhone wie eine iPhone-App, auf dem iPad wie eine iPad-App und auf Android-Geräten wie eine Android-App aussieht. Mit Compiler-Direktiven kann man Teile des Codes auch ausschließlich für Android oder nur für iOS ausführen. Perfekt ist es aber keineswegs.
Für den nächsten Teil sollte man im Hinterkopf behalten, dass ich fast keine Vorerfahrung mit SwiftUI oder Compose habe.
SwiftUI-Komponenten können mitunter recht komplexes Verhalten ausdrücken, etwa eine Suchleiste, die je nach Gerät und Bildschirmgröße verschiedene Teile der UI ein- oder ausblendet. Das bedeutet, dass es wahrscheinlich viele implizite Erwartungen an dieses Verhalten gibt. Skip versucht, dieses Verhalten möglichst genau nachzubilden und diese Erwartungen zu erfüllen, aber das klappt natürlich nicht immer. Manche Dinge, von denen man in reinem SwiftUI erwarten würde, dass sie funktionieren, tun das mit Skip nicht. Manchmal ist das dokumentiert, oft aber nicht. Das kann frustrierend sein – besonders weil die Suche in der Skip-Dokumentation nicht sehr gut funktioniert. Häufig tippt man einen Begriff ein, klickt auf einen Vorschlag und landet irgendwo völlig Unpassendem. Dadurch kann es sehr mühsam werden herauszufinden, welche Teile der SwiftUI-API tatsächlich unterstützt werden.
Einige Teile von SwiftUI werden gar nicht unterstützt. Das kann etwas lästig sein, aber dann muss man eben mit Compiler-Direktiven ein Feature auf Apple-Geräten mit vollem SwiftUI und auf Android auf eine andere Weise implementieren. Skip unterstützt auch, Jetpack-Compose-Komponenten direkt innerhalb von SwiftUI-Komponenten zu erstellen. Ohne Compose-Erfahrung fand ich das unintuitiv und habe es deshalb meist vermieden. Stattdessen habe ich die Android-Implementierung an SwiftUI-Funktionen angepasst, die Skip unterstützt.
Da ich den Transpiler-Modus verwende, muss man außerdem berücksichtigen, dass praktisch alle Swift-Bibliotheken nicht mit Skip kompatibel sind. Für mich war das größtenteils kein Problem; die einzige Ausnahme waren die Supabase-Client-Bibliotheken. Für solche Fälle ermöglicht Skip die Verwendung von Kotlin-Bibliotheken mit Swift-Syntax. Auch das kann wegen Unterschieden bei Datentypen sowie fehlender Code-Vervollständigung, Parameterinformationen und Ähnlichem schwierig sein. Xcode wurde schließlich nie dafür entwickelt, Kotlin-Bibliotheken über Swift-Syntax aufzurufen. Der Compile-Modus sollte in dieser Hinsicht weniger problematisch sein, weil viele Swift-Bibliotheken unverändert verwendet werden können.
Komplexität
All diese Dinge sind ziemlich gut handhabbar, sobald man sich mit Skip auskennt. Etwas problematischer ist Folgendes: Wahrscheinlich hatten alle SwiftUI-Entwickler schon zumindest ein wenig Reibung mit dem Swift-Type-Checker – mit dem gefürchteten Fehler „The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions“. Im Allgemeinen bedeutet das, dass man Code in kleinere Komponenten aufteilen muss. Das ist grundsätzlich eine gute Praxis und hat in SwiftUI keine weiteren echten Nebenwirkungen.
Bei Skip-/Compose-Komponenten ist das allerdings etwas anders. Nehmen wir an, wir haben eine Liste und in jedem Listenelement befindet sich ein Button.
In normalem SwiftUI können wir den Button in einen weiteren View packen und ihn in eine andere Datei auslagern – zum Beispiel als CustomButton-Komponente mit besonderen Icons oder Ähnlichem.
Trotzdem passt sich der Button weiterhin daran an, in einem Listenelement zu stehen. Dasselbe gilt für Android, solange der Button direkt in der Liste platziert ist. Verpackt man ihn aber in einen weiteren View, um daraus eine eigenständige Komponente zu machen, gilt das möglicherweise nicht mehr. Das kann in manchen Situationen egal sein. Problematisch wird es aber zum Beispiel dann, wenn man die Position eines Listenelements innerhalb eines ScrollViewReader nur abfragen kann, wenn dieses Element ein direktes Kind der List ist, die wiederum ein direktes Kind des ScrollViewReader ist.
Kurz gesagt hat die Funktionsweise von Skip meinen Code manchmal komplexer gemacht, als ich es mir gewünscht hätte. Ja, mit ViewBuilder lässt sich Code aufteilen, ohne ihn in zusätzliche Views zu packen, aber ideal ist auch das nicht.
Wie bereits erwähnt, wird der Swift-Compiler – und besonders seine Fehlermeldungen – zunehmend nutzlos, je komplexer die SwiftUI-Konstrukte werden. Ein einfacher Fehler, etwa ein falscher Parametername in einem Methodenaufruf, kann den gefürchteten Fehler „The compiler is unable…“ auslösen. Das zwang mich oft, Abschnitt für Abschnitt auszukommentieren, bis das Projekt wieder kompilierte, nur um meinen Fehler einzugrenzen.
Ein weiterer Punkt sind die Compile-Zeiten. Um die App zu testen, muss sie gebaut werden. Das dauert nun doppelt so lange, weil die App statt für eine für zwei Plattformen gebaut werden muss – logisch. Außerdem gibt es kein Hot Reload oder Ähnliches.
Zur Erinnerung: Erfahrung mit SwiftUI und Compose ist wirklich sehr hilfreich. Sie hilft dabei, die kleinen Unterschiede zu erkennen, die von einem leichten Performanceverlust bis zu einer völlig unbenutzbaren App alles verursachen können. Ich hatte diese Erfahrung nicht, was ein wichtiger Grund dafür sein kann, warum ich die Probleme so wahrgenommen habe.
Fazit
Auch wenn es Probleme gab, denke ich, dass die Wahl von Skip für dieses Projekt richtig war. Es macht deutlich mehr Spaß als zum Beispiel Ionic, und die Ergebnisse sprechen für sich.
Sobald man seine Eigenheiten kennt, kann man ziemlich effektiv damit arbeiten. Man bekommt tatsächlich zwei vollständig native Apps zum Preis – im Sinne des Entwicklungsaufwands – von 1,2 Apps. Es ist immer noch mehr Arbeit, als nur eine App für eine einzelne Plattform zu entwickeln, aber deutlich weniger als zwei Apps, je eine für jede Plattform. Angesichts dessen erscheinen mir die Preise von 29 $/Monat pro Entwickler für kleine Unternehmen und 99 $/Monat für größere Unternehmen fair. Man bezahlt außerdem nur für die Build-Werkzeuge, also das SkipStone-Build-Plugin. Die Bibliotheken selbst sind Open Source, man muss die Lizenz also nicht weiter bezahlen, um die Apps online lassen zu dürfen.
Nachdem ich einen vollständig webbasierten Ansatz mit Capacitor + Ionic und nun einen vollständig nativen mit SwiftUI + Skip ausprobiert habe, werde ich für mein nächstes mobiles Projekt wahrscheinlich React Native versuchen – vielleicht auch Kotlin/Compose Multiplatform, wer weiß 🤷♂️.
Da Vector Bible – eine Vue.js-Web-App plus diese native App – aber fast ein Jahr Entwicklungszeit beansprucht hat, wird mein nächstes Projekt wahrscheinlich weniger komplex. Und mit Liquid Glass, das unmittelbar bevorsteht, muss ich diese App auch noch daran anpassen. Die Nachwirkungen des gesamten Vector-Bible-Projekts sind allerdings ein Thema für ein anderes Mal. Vermutlich sobald die Dinge, an denen ich zuletzt gearbeitet habe, endlich freigegeben und veröffentlicht sind.
Vielen Dank fürs Lesen!