Creating a Cospend app with SwiftUI and Combine

In unserem Münchner Büro hatten wir ein Problem, das bestimmt viele Firmen kennen: Beim Mittagessen gehen oder wenn man abends mal Essen bestellt wird Geld ausgelegt und dann als Bargeld hin- und her gereicht oder „gepaypalt“ – der ständige Transfer von Geld und die dadurch entstandene Unübersichtlichkeit nerven leider ziemlich.

Es gibt eine Reihe von Software-Lösungen, um das gegenseitige Auslegen von Geld in der Gruppe zu tracken und zu verrechnen, z. B. Splitwise, Kittysplit oder iHateMoney. Es war von vornherein klar, dass wir eine Open-Source-Lösung wollen. Bei uns fiel die Wahl auf Cospend, da es als Nextcloud-App verfügbar ist die schnell aufzusetzen ist und bei der die Daten in unserer Nextcloud gehostet werden, statt auf einem externen Server. Cospend ist übrigens von iHateMoney inspiriert.

Zu Cospend gibt es sowohl ein in Nextcloud integriertes Web-Interface, als auch eine Android-App, über die Rechnungen auch mobil eingetragen werden können. Leider gab es bis jetzt keine entsprechende iOS-App, so dass unsere iOS-User unterwegs nicht so bequem neue Einträge erstellen konnten.

Deshalb haben wir uns entschieden, zu zweit den Zeitraum zwischen zwei Projekten zu nutzen, eine iOS-App zu entwickeln. Ziel dabei war auch, mit den neuen Frameworks SwiftUI und Combine zu arbeiten, ein neues Stück Open-Source-Software für Mayflower zu erschaffen und unser Portfolio im Mobile-Bereich zu erweitern.

Product Backlog

Die Android-App mit dem Namen Moneybuster bildet alle Features der Weboberfläche ab und hat sehr viele Funktionen. Da wir nur begrenzt Zeit hatten, haben wir uns darauf konzentriert, die für die Userbase in unserem Umfeld essenziellen Features zu implementieren. 

Um sicherzugehen, dass wir eine wertvollen App erschaffen, bevor wir wieder auf Kundenprojekten arbeiten, sind wir streng nach Scrum vorgegangen und haben in Sprints geplant. Dabei sollte der erste Sprint bereits einen Minimal Viable Prototype (MVP) zum Resultat haben, den wir hätten benutzen können.

Der MVP konnte ausschließlich ein bestehendes Projekt (so heißen Nutzergruppen bei Cospend) in der App speichern, bestehende Rechnungen im Projekt anzeigen und neue hinzufügen. Im letzten Sprint gelang es uns dann sogar, Support für iHateMoney hinzuzufügen, da es eine sehr ähnliche API zu Cospend hat.

Features

Folgende Features haben wir in unserer App PayForMe umgesetzt:

  • Anzeige von eingetragenen Rechnungen
  • Anlegen einer neuen Rechnung
  • “Kontostand” aller Gruppen-Mitglieder
  • Verwalten von mehreren Projekten
  • Farbcodes zum Unterscheiden einzelner Nutzer
  • Bearbeiten von vorhandenen Rechnungen
  • Löschen von Rechnungen
  • Schuldenausgleich einzelner Nutzer
  • Erstellen von neuen iHateMoney-Projekten
  • Hinzufügen von neuen Nutzern

PayForMe

Laden im App StorePayForMe findet ihr im Apple App Store …
… und den Code natürlich auf GitHub.

Tech-Stack

PayForMe ist eine native iOS-App, mit Swift in XCode geschrieben. Ziel war, dabei neue Technologien zu verwenden, um ihre Tauglichkeit für kommerzielle Projekte zu testen. Als UI-Framework setzen wir deshalb SwiftUI ein, für Event-Handling und Netzwerkkommunikation Combine. Für Continuous Deployment benutzen wir Fastlane, da es einfach und schnell aufzusetzen war und für unsere Zwecke komplett ausreicht.

SwiftUI

Letzten Sommer hat Apple das SwiftUI-Framework veröffentlicht, welches das aktuelle iOS-UI-Framework UIKit und den Interface Builder ablösen sollen. Ziel des Cospend-Projektes war auch, SwiftUI zu evaluieren, um zu testen, ob wir es zukünftig in Kundenprojekten einsetzen können. In aktuellen Kundenprojekten haben wir uns noch dagegen entschieden.

Das auffälligste Merkmal von SwiftUI ist sicherlich, dass der UI-Code in Swift geschrieben ist. So sieht die gesamte UI für eine Hello-World-App in SwiftUI aus:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

Layouts

Ein großer Unterschied zu UIKit ist, dass man als Entwickler sehr viel weniger Layout-Constraints definieren muss. In SwiftUI sind die üblichen Anordnungen als defaults gesetzt, und Abweichungen werden CSS-ähnlich übergeben, z. B.:

Text("Hello, World!").bold()

oder

Text("Hello, World!").font(.headline)

Beide Zeilen machen im default das Gleiche.

State Management

SwiftUI ist deklarativ, d .h. man beschreibt den Ist-Zustand der App unter Berücksichtigung des aktuellen States (das ganze State-Management-System erinnert an React). Bei Veränderung der UI-Elemente wird die View geupdatet:

struct ContentView: View {
    
    @State
    var counter = 0
    
    var body: some View {
        VStack { //Vertically aligns the Child views
            Text("Number button presses: \(counter)")
            Button(action: {
                self.counter += 1
            }) {
                Text("Count")
            }
        }
    }
}

Der Transfer von Daten funktioniert über das Binding von States in Subviews. Das sieht dann so aus:

struct ContentView: View {
    @State
    var counter = 0
    var body: some View {
        VStack {
            Text("Number button presses: \(counter)")
            CounterButton(counter: $counter)
        }
    }
}

struct CounterButton: View {
    @Binding
    var counter: Int
    var body: some View {
        Button(action: {
            self.counter += 1
        }) {
            Text("Count")
        }
    }
}

Das $-Zeichen bedeutet, dass ein Binding auf den State übergeben wird und nicht die eigentliche Integer-Variable. D. h. in CounterButton existiert eine Referenz auf den State, der in ContentView existiert. Das wiederum bedeutet, dass jede Veränderung von counter in einem der referenzierenden Subviews den State verändert und zu einer Neuberechnung der UI führt.

Fazit und Probleme

SwiftUI ist meiner Meinung nach leider noch nicht Production-ready. Das liegt unter anderem an folgenden Gründen:

  • Abwärtskompatibilität: SwiftUI ist erst ab iOS 13 verfügbar. Je nach Art der App ist das ein größeres oder kleineres Constraint. Ist die App das Produkt, ist es vielleicht okay, sich auf 93 Prozent der potenziellen User zu beschränken. Ist die App Teil eines Services, wie z. B. Carsharing oder Mobile Banking, ist das wahrscheinlich kritischer.
  • Compiler Warnings: Kompiliert SwiftUI-Code nicht, wird häufig nicht der richtige Fehler und die richtige Zeile als nicht-kompilierend markiert, sondern eine zufällige andere. Das erschwert den Debugging-Prozess erheblich, soll aber angeblich mit XCode 11.4 besser werden.
  • Strange Bugs: „There are super strange bugs on some devices like this one which you don’t want to have in production. For me, this is the biggest no-go for SwiftUI.“

Combine

Parallel zu SwiftUI wurde das Combine-Framework auf der letzten WWDC eingeführt.

Combine ist ein deklaratives Framework zum funktionalen und reaktiven Programmieren in Swift. Es kann für User-Interaktion, Netzwerkkommunikation und alle Arten von anderen asynchronen Events verwendet werden. Combine ist gut in SwiftUI integriert, kann aber auch mit UIKit benutzt werden.

Auch andere Basis-Frameworks von Apple (wie URLSessions oder CoreData) wurden erweitert, um Combine zu integrieren. Z. B. würde man in Swift ohne Combine Daten aus einer API imperativ und mit Closures beispielsweise so empfangen:

URLSession.shared.dataTask(with: request, completionHandler: {
            data, response, error in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    print("Network error")
                    return
            }
            var members = [Person]()
            if let data = data,
                let decodedMembers = try? self.decoder.decode([Person].self, from: data) {
                members = decodedMembers
            }
            let membersdict = Dictionary(members.map {($0.id,$0)}) {
                a,_ in a
            }
            return membersdict
            }).resume()

Und mit Combine deklarativ und asynchron auf diese Art:

return URLSession.shared.dataTaskPublisher(for: request)
            .compactMap { data, response -> Data? in
                guard let httpResponse = response as? HTTPURLResponse,
                    httpResponse.statusCode == 200 else {
                        print("Network Error")
                        return nil
                }
                return data
        }
        .decode(type: [Person].self, decoder: decoder)
        .replaceError(with: [])
        .map {
            members in
            Dictionary(members.map {($0.id, $0)}) {a,_ in a }
        }
        .eraseToAnyPublisher()

Fazit zu Combine

Combine ist ein modernes Werkzeug, um asynchrone Events sauber und gut leserlich zu behandeln. Man schreibt nicht unbedingt weniger Zeilen an Code, aber wenn man fertig ist, kann man sich deutlich sicherer sein, dass der Code keine ungewünschten Nebeneffekte hat. Außerdem sind die Lesbarkeit und Kapselung meiner Meinung nach deutlich besser. 

Combine wirkt sehr viel “fertiger” als SwiftUI und kann ohne größere Probleme in Production-Environments eingesetzt werden. Das einzige wirkliche Hindernis daran wäre die viel zu knappe Dokumentation von Apple, welche durch das offene Buch Using Combine von Joseph Heck ausgeglichen wird, das nahezu sämtliche Features und Komponenten ausführlichst erklärt.

… und in Kombination

Im Laufe der Entwicklung wurde ich von zwei Dingen überrascht: Wie unfertig SwiftUI noch ist und wie fertig Combine im Vergleich erscheint. Gleichzeitig war es allerdings gut möglich, dass wir uns in beide Frameworks zügig einarbeiten konnten. Wenn SwiftUIs Kinderkrankheiten behoben sind, ist es definitiv im Vergleich zu UIKit weit überlegen.

Auch der klassische 80/20- bzw. 90/10-Verlauf eines Softwareprojekts ließ sich gut nachvollziehen: Man könnte noch Monate sinnvoll an der App weiterentwickeln, die Core-Features standen allerdings schon nach zwei Wochen.

In Hinsicht auf sinnvoll investierte Zeit im Leerlauf zwischen zwei Projekten würde ich PayForMe definitiv als Erfolg werten, da es uns einen tiefen Einblick in zwei neue Frameworks geliefert hat, echten Mehrwert für einige unserer Mitarbeiter, unser vorzeigbares Portfolio für Mobile Apps erweitert und unsere Open-Source-Kontribution erhöht.

Für neue Blogupdates anmelden:


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.