arrow arrow--cut calendar callback check chevron chevron--large cross cross--large download filter kununu linkedin magnifier mail marker media-audio media-blog media-video menu minus Flieger phone play plus quote share youtube

Skip

Crossplatform-Entwicklung aus der iOS-Richtung

Mobile Entwicklung dreht sich heutzutage in erster Linie um die zwei großen Betriebssysteme, iOS und Android. Beide Plattformen haben ihre eigene Programmiersprache, ihre eigenen Werkzeuge und eigenen Ansätze. Um beide dieser Umgebungen optimal ausnutzen zu können, werden bei App-Projekten daher üblicherweise zwei separate Apps entwickelt. Eine für Android, geschrieben in Kotlin, gebaut in Android Studio und eine für iOS, geschrieben in Swift, gebaut in Xcode. Dies ist technisch die sauberste Lösung und ermöglicht die Nutzung der jeweils modernsten Technologien, allerdings führt dieser Ansatz auch zu einem gewissen doppelten Aufwand bei der Umsetzung einfacher Features.

Um diesen Zusatzaufwand zu umgehen, gibt es mittlerweile eine Bandbreite von sogenannten Crossplatform Frameworks, die versprechen, aus einer einzigen Code-Basis heraus direkt zwei Apps für die beiden Plattformen zu erstellen und somit doppelte Implementierungen möglichst zu vermeiden. Wichtig bei der Entscheidung für oder gegen ein solches Crossplattform Framework ist es, mit welchem Code das jeweilige Framework arbeitet und welche Entwicklergruppe sich entsprechend mit dem Projekt befassen wird. React Native zum Beispiel basiert auf der Web-Technologie React und eignet sich daher gut, um Web-Entwickler in mobile Entwicklung einzubeziehen. Das Framework Kotlin Multiplatform stammt hingegen direkt von Google und basiert, wie der Name schon sagt, auf der Sprache Kotlin. Somit ermöglicht dieses Framework Android-Entwicklern die Crossplatform-Entwicklung mit der Sprache und den Werkzeugen, die ihnen bereits bekannt sind.

Von iOS-Seite aus fehlt bisher ein solcher offizieller Ansatz. Ein kleines Team von unabhängigen Entwicklern hat sich mittlerweile aber der Herausforderung gestellt und eine neue Crossplatform-Lösung ins Leben gerufen, mit dem unscheinbaren Namen:
Skip.

Foto von Timo Rausch
Timo Rausch

iOS Developer

Skip

Skip basiert auf Apples Programmiersprache Swift und verwendet in erster Linie die Apple IDE Xcode. Entwickler, die sich im Apple- bzw. iOS-Umfeld bewegen, kennen sich also direkt aus. Über die verwendete Programmiersprache hinaus bietet Skip jedoch auch noch eine weitere Besonderheit gegenüber anderen Frameworks an: Skip verwendet für die Crossplatform-Entwicklung das sogenannte Skip Transpiler Plugin. Dieser Transpiler sorgt dafür, dass bei jedem Bauvorgang aus dem geschriebenen Swift-Code funktionierender Kotlin-Code generiert wird. Diesen generierten Kotlin-Code kann man dann jederzeit einsehen und auch in Android Studio öffnen. Um dies zu ermöglichen, bietet Skip eine große Anzahl von Android-Klassen, die native iOS-Framework-Funktionen nachbilden. Bei der Transpilierung werden diese Kotlin-Klassen entsprechend verwendet, um ihr jeweiliges iOS-Pendant zu ersetzen.

Im Folgenden schildere ich nun ein paar meiner Erfahrungen mit Skip beim Implementieren eines kleinen Beispielprojektes. Konkret habe ich etwas UI in SwiftUI gebaut und zusätzlich ein Third-Party-Framework eingebunden, welches je eine iOS- und Android-Version besitzt.

Continuous Inspiration #4: Kotlin Multiplatform

Crossplatform Development im Podcast

Noch mehr Perspektiven auf Crossplatform Development gibt es auch in unserem Podcast Continuous Inspiration: In Folge vier diskutieren iOS Developer Konstantin und Adroid Developer Dimitri aus ihren jeweiligen Perspektiven über Kotlin Multiplatform. Was das mit dem Wilden Westen zu tun hat? Einfach reinhören!

Der Anfang mit Skip

Der Anfang mit Skip ist relativ schnell gemacht. Auf der Webseite von Skip befindet sich eine Anleitung zur Installation von Skip und seiner verschiedenen Software-Abhängigkeiten. Skip selbst enthält ein Checkup-Tool, mit dem sich leicht feststellen lässt, ob die nötigen Anforderungen alle richtig auf dem System installiert sind.

Um mit Skip Code kompilieren zu können, benötigt Skip, nach einer zweiwöchigen Testperiode, einen Lizenzschlüssel. Beim Beantragen des Lizenzschlüssels kann man zusätzlich einen einmonatigen Testschlüssel beantragen. Nach diesen bis zu sechs Testwochen werden anschließend für jeden Schlüssel laufende Kosten fällig. Das Geschäftsmodell von Skip basiert auf einer Bezahlung pro Entwickler pro Monat, wobei die genauen Kosten je nach Unternehmensgröße variieren. Ohne eine gültige Lizenz ist das Bauen und Transpilieren der App dann nicht mehr ohne weiteres möglich.

Die Dokumentation von Skip findet sich ebenfalls auf der Homepage. Hier gibt es kurze Beispiele zur Verwendung zahlreicher iOS-Frameworks, generelle Hinweise zu grundlegenden Themen wie Debugging, Platform Customization sowie Dependency Management. Die Dokumentation ist meist anständig, jedoch gab es durchaus Themen, wo ein Blick in die Beispiel-Apps hilfreich war.

Nach der Einrichtung von Skip kann man nun ein Beispielprojekt erstellen, indem man einfach den folgenden Konsolenbefehl verwendet:

skip init --open-xcode --appid=com.xyz.HelloSkip hello-skip HelloSkip

Dieses Beispielprojekt bietet eine Tab-Bar über die man zwischen zwei Views hin und her navigieren kann. Der Code ist überwiegend SwiftUI und enthält an einer Stelle eine Unterscheidung, um die UI auf Android geringfügig anders aussehen zu lassen. Das Projekt sollte nach diesem Konsolenbefehl sofort einsatzbereit sein und kann in Xcode geöffnet werden. Bevor man bauen kann, muss jetzt nur noch in Android Studio ein Android Simulator gestartet werden. Wenn man nun in Xcode den Play-Button betätigt, sollte die Beispiel-App erfolgreich gebaut werden. Daraufhin sollte sie sich automatisch in einem iOS-Simulator sowie in dem gestarteten Android-Simulator öffnen.

Mit diesem grundlegenden Projekt-Setup kann man im Anschluss weiterarbeiten. Bei meinen Experimenten musste ich, abgesehen von Appname und App-ID, bisher keine Änderungen an den Grundeinstellungen des Projektes vornehmen.
 

Debugging

Zum Debugging der App kann man für iOS in Xcode mit Breakpoints und Konsolenoutput arbeiten und hat das volle Feature-Set der IDE zur Verfügung. Für Android kann man den Konsolenoutput in Android-Studio mittels dem dort integrierten Programm Logcat anzeigen. Compiler-Warnungen und Fehler erhält man für Android Code nach dem Bauen auch in Xcode. Fehler werden sowohl im Android-Output als auch in den entsprechenden Zeilen im Swift-Code hervorgehoben. Zum Debuggen lohnt es sich häufig auch, einen Blick in den erzeugten Kotlin-Code zu werfen. Wenn man in Xcode im Project Navigator einen Rechtsklick auf das App-Paket macht, dann gibt es dort eine Option "skip"-> "Create SkipLink". Führt man diese Option aus, werden die generierten Kotlin Dateien in Xcode indexiert, sodass sie mit der Option "Open Quickly" (CMD+Shift+O) als .kt Dateien gefunden und geöffnet werden können.

UI mit Skip

UI lässt sich mit Skip im Grundsatz relativ einfach bauen. Man nutzt dabei klassisch SwiftUI Komponenten und Skip übersetzt diese dann in äquivalente Jetpack-Compose Elemente von Kotlin. Etwas Komplexität entsteht jedoch trotzdem beim Einbinden der verschiedenen Ressourcen, die man für die UI braucht.

Lokalisierte Texte

Texte können, wie für iOS standardmäßig, in einer Lokalisierungs-Datei hinterlegt werden. Diese befindet sich in Skip unter "Sources/<Projektname>/Resources/Localizable>". Allerdings funktioniert für Android die Methode "String(localized: , bundle: )" teils nur eingeschränkt. Besser funktioniert es, einen SwiftUI "Text" direkt mit dem entsprechenden String zu befüllen.

Bilder

Eigene Bilder können wie nativ in dem Xcode Asset-Katalog "Sources/<Projektname>/Resources/Module>" hinterlegt werden und mit einem SwiftUI Image-Objekt ("Image("<Bildname>", bundle: .module)") entsprechend angezeigt werden. Zu beachten gibt es, dass Android mit Bildern nicht klarkommt, die im Dateisystem manche Sonderzeichen oder Leerstellen verwenden. Der eigentliche Dateiname ist in Xcode durch den Asset-Katalog verborgen, daher muss man besonders darauf achten.

Systembilder funktionieren nur begrenzt. "Image(systemName: "<Bildname>")" kann manchmal funktionieren, "person.fill" ergibt z. B. auf beiden Plattformen ein sinnvolles Bild. Bei vielen anderen funktioniert dies aber meiner Erfahrung nach leider nicht.

Das Einfärben von Bildern und Icons ist auf iOS einfach mit der entsprechenden tint Funktion möglich. Auf Android wird stattdessen anscheinend die foreground-Farbe benutzt. Damit bei einem selbst hinzugefügten Image die Farbe bei Android entsprechend angepasst wird, muss man im Asset-Katalog für das Bild die Einstellung "Render as: Template Image" setzen.

Fonts

Custom fonts müssen für jede Plattform einzeln hinterlegt werden. Auf iOS muss man sie dem Projekt hinzufügen und im Folder "Sources/<Projektname>/Resources/" ablegen. Des Weiteren ist es nötig, die custom-Fonts in der Build-Phase Copy Bundle Resources hinzuzufügen und in der info.plist die Fonts zusätzlich unter dem Key "Fonts provided by application" zu registrieren. Es scheint nicht möglich zu sein, die Fonts zur Laufzeit der App zu registrieren. Um sie auf Android verfügbar zu machen, müssen die Fonts lediglich in den Ordner "Android/app/src/main/res/font" kopiert werden. Daraufhin können die Fonts sofort verwendet werden.

Farben

Mir ist es nicht gelungen, Custom-Farben über den Asset-Katalog von Xcode zu verwalten. Ich habe stattdessen manuell eine Klasse zur Farbverwaltung geschrieben, die die entsprechenden Farbobjekte mit hart-codierten RGB-Werten initialisiert. Dort habe ich dann auch manuell eine Unterstützung für den Darkmode eingebaut, indem ich auf die Environment-Variable "colorScheme" meiner SwiftUI Views reagiert habe. Dies hat durch die Transpilierung für beide Plattformen ohne weitere Anpassungen funktioniert.

Programmieren in Skip

Das Ziel von Skip ist das einmalige Schreiben von Code in Swift. Dieser soll dann durch Transpilierung auch auf Android verfügbar gemacht werden. Dabei ist man jedoch teilweise in seinen Möglichkeiten im iOS-Kontext etwas eingeschränkt. Skip hat zwar bereits einen recht breiten Support für diverse Frameworks von Apple. Dennoch kommt es bei der Umsetzung von fast jedem Feature, das man realisieren will, im Schnitt einmal vor, dass eine nützliche, häufig neue, Funktion einfach nicht für die Transpilierung zur Verfügung steht. Dann steht man vor der Frage, ob man nun diese nützliche Swift-Funktion für den iOS-Teil dennoch nutzt und für Android alternativen Code schreibt, oder ob man einfach einen einzigen Workaround für beide Plattformen verwendet. Insbesondere beim Einbinden von Third-Party Android Frameworks bleibt aber oft keine andere Möglichkeit, als eine konkrete Android-Implementierung zu definieren.

Mit Skip kann man Android-spezifischen Code auf drei verschiedene Weisen realisieren:

  1. Transpilierung
    Durch die Präcompiler-Anweisung "#if Skip" ist es möglich, Swift-Code zu schreiben, der aber nur in der Android-Version verwendet wird. So kann man auch auf Android-spezifische Funktionen und Bibliotheken zugreifen, die in Swift zu einem Kompilierfehler führen würden. Unterstützung beim Code-Schreiben durch die Auto-Completion erhält man allerdings nur für das Ansprechen von iOS-spezifischen Funktionen. Auch Compiler-Warnungen tauchen für Android-Funktionen nicht während des Programmierens auf. Spezielle Android-Funktionen implementiert man in diesem Fall also nur basierend auf Compiler-Warnungen und Fehlern, die beim Bauen entstehen. Diese Kotlin-Fehler werden aber zusätzlich direkt im Swift-Code in der entsprechenden Zeile angezeigt, was den Umgang etwas einfacher macht.
  2. Codekommentare
    In Skip gibt es spezielle Codekommentare z. B. "/* SKIP INSERT" oder "// SKIP REPLACE". Diese ersten Zeilen eines Kommentars sorgen dafür, dass der Compiler die nachfolgenden Zeilen des Kommentars als natives Kotlin direkt in den Kotlin-Output rüberkopiert, ohne dass Transpilierung stattfindet. Dies führt jedoch zu etwas unübersichtlichem Code, weil nun auskommentierte Teile des Codes tatsächlich zum Funktionsumfang der App beitragen. Zusätzlich zu dem eben erwähnten Fehlen der Autocompletion und sofortiger Compiler Warnungen in XCode für Kotlin-Funktionen kommt nun erschwerend hinzu, dass alle Warnungen und Fehler nach dem Bauen in der Swift-Datei nun am Ende des Kommentars angezeigt werden anstatt in der entsprechenden Zeile aufzutauchen. Bei diesem Vorgehen sollte man stattdessen also direkt den Kotlin-Output inspizieren, wo die Fehlermeldungen tatsächlich an der richtigen Stelle stehen.
  3. Dateien in Kotlin schreiben
    Es ist möglich, komplette Dateien einfach in Kotlin zu schreiben. Dies hat dann den Vorteil, dass man das File in Android Studio anpassen kann und etwas mehr Unterstützung von der IDE erhalten kann.

Imports innerhalb der Files müssen bei allen drei Arten von Hand gepflegt werden, da Xcode hier nicht unterstützen kann. Nur bei der Verwendung von puren Kotlin-Dateien, kann man diese in Android-Studio öffnen, wo dann Unterstützung für die Imports zur Verfügung steht.
Grundsätzlich ist es möglich zwischen diesen drei Techniken beliebig zu kombinieren. Es ist also zum Beispiel möglich die SKIP INSERT-Methode zu verwenden, um Kotlin-Code zu schreiben, für den es in Swift kein Pendant gibt, den Rest eines Files aber in Swift zu schreiben die Transpilierung zu nutzen.

Frameworks

Beim Einsatz von Third-Party Frameworks gibt es oft eigene Versionen pro Plattform. Diese können mit Skip für beide Plattformen separat eingebunden werden. Auf iOS können Frameworks wie gewohnt über den Swift Package Manager verwaltet und im Code gewöhnlich verwendet werden. Im Code kann mit der Präcompileranweisung "#if !SKIP" bzw. "#if SKIP <…> #else"  purer Swift-Code geschrieben werden, der nicht auf die Fähigkeiten von Android oder Android-Frameworks Rücksicht nehmen muss. In diesem Block kann man also eine iOS-spezifische Framework-Version ansprechen.

Für Android ist das Einbinden von nativen Android Frameworks ein Stück weit komplizierter. Das Dependency Management von Android geschieht wie üblich über Gradle und manchmal Maven durch Gradle. Anstatt jedoch direkt Gradle Build- und Settingsfiles zu modifizieren, bietet Skip eine Datei namens "skip.yml" in einem Ordner "Skip". In dieser Konfigurationsdatei kann man dann anhand einer Baumstruktur festlegen, wo den Gradle Settings- und Build-Files Einträge hinzugefügt werden sollen. Diese Struktur unterscheidet sich von Standard-Gradle und ist daher etwas unhandlich. Man muss somit erst herausfinden, wie die Gradle Konfiguration am Ende auszusehen hat und anschließend muss man davon ableiten, wie man das Skip-File aufbauen muss, um zu diesem Ergebnis zu gelangen. Immerhin bietet Skip standardmäßigen Support für Maven in Gradle, was einige verschachtelte Imports ersparen kann.

Konfigurationsdateien

Generell ist der Zugriff auf Dateien über die Standard-Systemfunktionen möglich. Sollte ein Android Framework konkret auf das Android-eigene R-Framework angewiesen sein, so muss man Dateien entsprechend in dem Ordner Sources/<Projekt-Name>/res/raw ablegen. Dann kann man in Kotlin-Code mit dem Aufruf "R.raw.<Filename>" die Datei im Code verfügbar machen.

Android Context und Activities

Einige Android-Funktionen sind bei ihrer Implementierung auf die konkreten UI-Elemente von Android angewiesen. Glücklicherweise bietet Skip hier zwei besondere Funktionen für die Transpilierung an, die dieses Problem lösen. In einem "#if SKIP"-Block gibt es von Skip extra die zwei entsprechenden Funktionen: "ProcessInfo.processInfo.androidContext" und "UIApplication.shared.androidActivity".

Fazit

Skip bietet eine einfache Möglichkeit als iOS-Entwickler zügig mit Crossplattform-Entwicklung anzufangen. Der Ansatz hat einige Einschränkungen und Eigenheiten, die man beim Programmieren regelmäßig beachten muss. Dennoch gibt es meistens einen Weg zum Ziel. Verglichen mit nativer Entwicklung leidet durch diese Kompromisse etwas die Codequalität. Gleichzeitig spart man sich durch den Crossplattform-Ansatz aber auch einige doppelte Aufwände.

Als jemand der bislang keine große Kenntnis von Kotlin hatte, kann ich darüber hinaus festhalten, dass das vereinzelte Schreiben von Kotlin-Zeilen kein besonders großes Hindernis dargestellt hat. Swift und Kotlin sind sich oberflächlich relativ ähnlich. Natürlich unterscheiden sie sich in vielen fortgeschrittenen Features und in ihren Frameworks, aber mit einem fundierten Hintergrund in iOS-Entwicklung war es mir meistens möglich, mich trotzdem zeitnah zurecht zu finden. Fragen wirft vor allem der Closed-Source-Ansatz des Transpiler-Plugins mit der entsprechend nötigen Lizensierung auf, da man hier für die fortlaufende Entwicklung seiner App auf das Entwickler-Team von Skip angewiesen bleibt. 

Insgesamt war es für mich aber ein interessanter Ausflug in die Crossplattform-Welt.

Maßgeschneiderte App-Lösungen

Ob nativ in iOS oder Android, crossplatform oder als Web App: Die Lösung muss zum Produkt passen. Hier geht's zu unserem App und Frontend Development Portfolio!