Firebase – Lesen, Schreiben, Ändern und Löschen

Inhaltsverzeichnis

Firebase ist eine cloudbasierte Datenbanklösung von Google. Mit der Hilfe von Firebase kann man Daten in der Firebase Cloud lesen, schreiben, ändern und löschen. Diese vier grundlegenden Funktionen habe ich als Übung in einer Swift App genutzt. Ich habe eine App geschrieben, mit der man eine Leseliste führen kann. Dabei habe ich die Datenstruktur einfach gehalten und mich auf den Titel und Preis eines Buches beschränkt. Die App folgt dem MVVM (Model View View-Model) Konzept.

 

Model

Als Model habe ich eine Buchstruktur gewählt. Ein Buch sieht folgendermaßen aus.

 
				
					struct Buch: Identifiable{
    
    var id = UUID()
    var titel: String
    var preis: Double
    
}
				
			

Ein Buch folgt dem Protokoll „Identifiable“. Dieses Protokoll setzt voraus, dass das Objekt eine „id“ hat, an der es identifiziert werden kann. Das ist notwendig, um mehrere Bücher in einer Liste anzuzeigen.

Die Attribute beschränken sich in diesem Beispiel auf den Titel und Preis eines Buches. 

 

View-Model

Das „View-Model“ ist etwas komplexer. Eine weitere Besonderheit ist, dass hier „Firebase“ importiert werden muss.

Damit ein einzelner Code-Block nicht zu lang wird, habe ich zuerst einmal nur die Namen der Methoden angezeigt. Was die einzelnen Methoden machen, werde ich im Anschluss erklären. 

 
				
					import Firebase

class BuchViewModel: ObservableObject {
    
    @Published var buecher: [Buch] = []
    private var db = Firestore.firestore()
    
    func datenLaden(){
        ...
    }
    
    func neuesBuchSpeichern(titel: String, preis: Double){
        ...
    }
    
    func datenLoeschen(buch: Buch){
        ...
    }
    
    func datenUpdaten(altesBuch: Buch, neuesBuch: Buch){
        ...
    }
}

				
			

Die Klasse „BuchViewModel“ folgt dem Protokoll „ObservableObject“. Dadurch ist es später möglich eine einzige Instanz des Objektes durch die gesamte App zu verwenden. Es entstehen also keine doppelten Objekte und wenn es Änderungen gibt, werden diese Automatisch von dem „View“ angezeigt.

Damit dies funktioniert muss das Attribut „buecher“ veröffentlicht werden. Dies geht durch den Ausdruck „@Published“

Es gibt noch ein weiteres Attribut, dies ist die Referenz zur Datenbank. Datenbank ist hier mit „db“ abgekürzt. 

Kommen wir als Nächstes zu den einzelnen Funktionen.

Daten laden

				
					func datenLaden(){
        db.collection("Leseliste").addSnapshotListener { querySnapshot, error in
            if let dokumente = querySnapshot?.documents {
                self.buecher = dokumente.map { firebaseDokument -> Buch in
                    let daten = firebaseDokument.data()
                    let entpackterName = daten["titel"] as? String ?? ""
                    let entpackterPreis = daten["preis"] as? Double ?? 0
                    
                    return Buch(titel: entpackterName, preis: entpackterPreis)
                }
            }
            else{
                print("Keine Antwort")
            }
        }
				
			

Das ist die Methode, die Daten lädt. Sie wird beim Starten der App aufgerufen. Durch den „SnapshotListener“ wird diese Methode auch immer aufgerufen, wenn sich Daten in der Datenbank ändern. 

Zuerst wird eine Sammlung (collection) mit dem Namen Leseliste aus der Datenbank gesucht. Zu dieser Sammlung wird dann der „SnapshotListener“ hinzugefügt.

Dadurch werden zwei Parameter zurückgegeben. Es wird eine Momentaufnahme oder Momentabfrage (querySnapshot) und ein Fehler (error) zurückgegeben. Beide diese Werte sind optional und müssen daher entpackt werden. 

Da es mir in dieser Beispiel-App hauptsächlich um das Laden, Schreiben, Ändern und Löschen von Daten ging, habe ich in diesem Beispiel nur darauf geprüft, dass die Momentaufnahme entpackt werden konnte. Fehler habe ich nicht ausgewertet. 

Nach dem Entpacken der Momentaufnahme wandle ich die erhaltenen Dokumente in Bücher Objekte um. Dies mache ich mit der „.map“ Methode. Diese nimmt ein Firebase Dokument und wandelt es in ein Buch um. Dabei erstelle ich zuerst eine Konstante für die Daten. Anschließend schreibe ich den Titel und Preis der Daten in die Konstanten „entpackterName“ und „entpackterPreis“. Wenn die Daten nicht als String beziehungsweise Double entpackt werden können, setze ich diese auf einen leeren String beziehungsweise 0. 

Im Anschluss gebe ich dann ein Buch Objekt zurück. Durch diesen Vorgang geschieht eine Umwandlung. Es wird ein Array (Sammlung) aus Firebase Dokumenten in einen Array aus Büchern umgewandelt. 

 

Daten speichern

Das Speichern von Daten ist sehr einfach und benötigt nur zwei Parameter. 

 
				
					func neuesBuchSpeichern(titel: String, preis: Double){
        db.collection("Leseliste").addDocument(data: ["titel":titel, "preis":preis])
    }
				
			

Die Funktion nimmt einen Titel und einen Preis als Parameter. Dann wird eine Verbindung zur Sammlung „Leseliste“ hergestellt. I Anschluss wird ein Dokument hinzugefügt. Dazu wird ein Wörterbuch (Dictionary) weiter gegeben. Ein Wörterbuch hat immer eine Zuordnung von Schlüssel und Wert. In diesem Fall muss das Wörterbuch folgende Form haben.

[String:Any]

Das bedeutet, dass der Schlüssel des Wörterbuches vom Typ String sein muss. Der Wert hingegen kann ein anderer Typ sein. Etwa ein Integer, Doublt oder Boolean. 

Daten löschen

				
					func datenLoeschen(buch: Buch){
        let anfrage = db.collection("Leseliste")
            .whereField("titel", in: [buch.titel])
            .whereField("preis", isEqualTo: buch.preis)
        
        anfrage.getDocuments { querySnapshot, error in
            if let querySnapshot = querySnapshot {
                for buch in querySnapshot.documents{
                    buch.reference.delete()
                }
            }
        }
    }
				
			

Um ein Buch zu löschen, erhält die Methode ein Buch. Nun muss ich mithilfe des Buches das Firebase Dokument finden, was durch das Buch repräsentiert wird. Dazu vergleiche ich den Titel und den Preis. 

Ich definiere eine Anfrage zur Sammlung „Leseliste“, bei der der Titel gleich den Buchtitel ist und der Preis gleich dem Buchpreis. Dann sende ich diese Anfrage ab und erhalte alle Dokumente, die der Anfrage entsprechen. Da es immer nur ein Buch mit dem gleichen Titel und Preis geben wird, erhalte ich nur ein Buch zurück. Ich entpacke also wieder die Momentaufnahme und lösche dann alle Dokumente, die in diese enthalten sind. 

Dabei ist mir Folgendes aufgefallen. Ich kann in der „for-Schleife“ auf „buch.data()“ zugreifen. Nicht aber auf „buch.delete()“. Um das Buch zu löschen, muss ich „buch.reference.delete()“ schreiben.

Daten ändern

Daten zu ändern, ist ähnlich wie Daten zu löschen. Der Grund dafür ist, dass man ebenfalls zuerst das Firebase Dokument finden muss. 

				
					func datenUpdaten(altesBuch: Buch, neuesBuch: Buch){
        let anfrage = db.collection("Leseliste")
            .whereField("titel", in: [altesBuch.titel])
            .whereField("preis", isEqualTo: altesBuch.preis)
        
        anfrage.getDocuments { querySnapshot, error in
            if let querySnapshot = querySnapshot {
                for document in querySnapshot.documents{
                    document.reference.setData(["titel":neuesBuch.titel, "preis":neuesBuch.preis])
                }
            }
        }
    }
				
			

Um Daten zu ändern, benötigen man das aktuelle Buch und das neue Buch. Da ich nicht weiß, welche Daten tatsächlich geändert worden sind, überschreibe ich die aktuellen Daten einfach mit den neuen Daten. 

Zuerst erstelle ich wieder eine Anfrage mit den Daten des alten Buches. Wenn ich das Dokument dann gefunden habe, überschreibe ich das Dokument mit den Daten des neuen Buches. 

Wenn Daten gespeichert, gelöscht oder geändert werden, löst das automatisch die Methode zum Laden der Daten aus. Der Grund liegt dabei an dem „SnapshotListener“ der auf diese Änderungen achtet. Das ist auch der Grund, dass ich im Anschluss an diese Methoden die Daten nicht manuell neu laden muss. Wenn das nicht der Fall wäre, müsste man in jeder Methode, die Daten ändert die „datenLaden“ Methode erneut aufrufen.

View

Jedes Projekt braucht einen Anfangspunkt. Dieser wird in Swift durch „@main“ gekennzeichnet. Dort wird auch der erste „View“ erstellt.

Einstiegspunkt

				
					import Firebase

@main
struct LeselisteMitFirebaseApp: App {
    
    init() {
        FirebaseApp.configure()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
				
			

Bei der Initialisierung wird die grundsätzliche Verbindung zu Firebase hergestellt beziehungsweise konfiguriert.

Zeile 10 erstellt dann den „Haupt-View“ mit, der den „View-Code“ enthält, der tatsächlich das User-Interface erstellt.

Haupt-View

Der „Haupt-View“ beinhaltet ein Objekt des „View-Model“und besteht aus zwei Tabs. Der erste Tab zeigt die Bücher in der Leseliste, der zweite Tab ermöglicht es neue Bücher zu dieser hinzuzufügen.

Zuerst eine vereinfachte Darstellung des Views. 

				
					struct ContentView: View {
    
    @ObservedObject var viewModel = BuchViewModel()
    
    @State var neuerBuchTitel = ""
    @State var neuerBuchPreis = ""
    @State var rueckmeldung = ""
    
    var body: some View {
        
        TabView{
            
            NavigationView{
                List{
                    ...
                }
                .navigationBarTitle("Leseliste")
                .onAppear(perform: {
                    viewModel.datenLaden()
                })
            }
            .tabItem {
                Image(systemName: "list.dash")
                Text("Leseliste")
            }
            
            VStack(spacing: 20){
                ...
            }
            .padding()
            .tabItem {
                Image(systemName: "plus.circle.fill")
                Text("Buch hinzufügen")
            }
        }
    }
    
    func buchLoeschen(at offsets: IndexSet){
        ...
    }
}
				
			

Damit das „View-Model“ immer aktualisiert wird, wenn sich die Liste der Bücher ändert, muss das „View-Model“ dem Protokoll „ObservableObject“ (Beobachtbares Objekt) folgen. Dies muss dem „View“ allerdings noch mitgeteilt werden. Dies geht durch den „Property Wrapper“ „@ObservedObject“. Damit werden alle Veränderungen der Bücherliste berücksichtigt und angezeigt. 

Zusätzlich gibt es noch drei weitere Variablen. Die Ersten beiden speichern einen neuen Buchtitel, und Preis, wenn ein neues Buch zur Leseliste hinzugefügt wird. Die dritte Variable speichert eine Rückmeldung. Diese zeigt dem Nutzer, ob ein neues Buch erfolgreich hinzugefügt werden konnte.

In Zeile 17 wird der Titel der Liste gesetzt. 

In Zeile 18 bis 20 wird die Methode „datenLaden“ aufgerufen. Damit werden die Daten aus der Firebase Datenbank in die App geladen. Zudem werden nach dem erstmaligen Aufrufen auch alle weiteren Änderungen widergespiegelt.

„TabItem“ erstellt ein Symbol und Text für den Tab, damit man diesen identifizieren kann.

Die Funktion „buchLoeschen“ wird von der Liste im ersten Tab aufgerufen, wenn eine Person einen Eintrag löscht. Diese Funktion ruft aber nur eine Methode aus dem „View-Model“ auf. Der Grund dafür ist, dass beim Aufrufen der Funktion nicht das Objekt selbst weitergegeben wird, sondern nur ein „IndexSet“. Diesen musste ich dann noch nutzen, um das Objekt zu finden.

Kommen wir nun zum ersten Tab.

Erster Tab

				
					NavigationView{
    List{
        ForEach(viewModel.buecher){ buch in
            NavigationLink(
                destination: UpdateView(viewModel: viewModel, buch: buch),
                label: {
                    Text(buch.titel)
                })
        }
        .onDelete(perform: buchLoeschen)
    }
    .navigationBarTitle("Leseliste")
    .onAppear(perform: {
        viewModel.datenLaden()
    })
}
.tabItem {
    Image(systemName: "list.dash")
    Text("Leseliste")
}
				
			

Zuerst gibt es einen „NavigationView“. Dieser ermöglicht es auf eine Zeile zu klicken und dann den Eintrag zu bearbeiten.

Die Liste zeigt die Leseliste an. Für jedes Buch in der Leseliste gibt es eine neue Zeile. Jede Zeile fungiert als „NavigationLink“ zu einem weiteren „View“. In diesem Fall ist dies der „UpdateView“. Dieser erhält das Buch der ausgewählten Zeile, sowie das „View-Model“. Es ist nötig, dass der View Zugriff darauf hat, um die Methode „datenUpdaten“ aufrufen zu können. 

In Zeile 10 wird die Option hinzugefühgt ein Buch aus der Liste zu löschen. Dazu wird die Funktion „buchLoeschen“ aufgerufen. Das Besondere dabei ist, dass die Funktion keine direkten Parameter nimmt. Trotzdem hat die Funktion Zugriff auf einen „IndexSet“. 

 

Zweiter Tab

Dieser Tab ermöglicht es dem Nutzer ein neues Buch zur Leseliste hinzuzufügen. 

 
				
					VStack(spacing: 20){
    TextField("Buch Titel", text: $neuerBuchTitel)
    TextField("Buch Preis, Bsp: 10.99", text: $neuerBuchPreis)
    
    Text(rueckmeldung)
    
    Button("Buch speichern"){
        if neuerBuchTitel.count != 0 , let entpakterBuchPreis = Double(neuerBuchPreis){
            viewModel.neuesBuchSpeichern(titel: neuerBuchTitel, preis: entpakterBuchPreis)
            rueckmeldung = "Erfolgreich gespeichert."
            neuerBuchTitel = ""
            neuerBuchPreis = ""
        }
        else{
            rueckmeldung = "Ein Fehler ist aufgetreten."
            neuerBuchTitel = ""
            neuerBuchPreis = ""
        }
    }
}
.padding()
.tabItem {
    Image(systemName: "plus.circle.fill")
    Text("Buch hinzufügen")
}
				
			

Der „View“ besteht aus zwei Textfeldern, einem Text und einem Button. 

Die beiden Textfelder sind zum Eintragen des Titels und Preises gedacht. Im Feld für den Preis steht ein Beispiel, da ein Punkt verwendet werden muss und keine Währung angegeben werden darf. Ansonsten ist es nicht möglich diese Eingabe in einem Double zu speichern. 

Der Text für die Rückmeldung ist anfangs leer. 

Wenn der Button gedrückt wird, wird überprüft, ob der Name des Buches mindestens ein Zeichen hat. Zudem wird überprüft, ob der Preis in einem Double gespeichert werden kann. Ist beides möglich, wird ein neues Buch im „View-Model“ gespeichert. Anschließend wird die Rückmeldung gegeben, dass das Buch gespeichert werden konnte. Dann wird der Buchtitel und Preis zurückgesetzt. Somit könnte der Nutzer direkt ein weiteres Buch hinzufügen.

Wenn der Titel und Preis nicht umgewandelt werden konnten, wird die Fehlermeldung „Ein Fehler ist aufgetreten“ angezeigt. Zudem werden auch die Felder für Titel und Preis zurückgesetzt. 

 

Update-View

Dieser „View“ wird angezeigt, wenn ein Nutzer in der Leseliste auf ein Buch drückt. Damit wird die Möglichkeit gegeben, dieses zu bearbeiten. 

Der Grundsätzliche Aufbaue ist ähnlich zum zweiten Tab. Der Unterschied ist hierbei allerdings, dass der Name und Preis bereits in den Textfeldern stehen und beim Speichern ein vorhandenes Buch-Objekt geändert wird. 

 
				
					struct UpdateView: View {
    
    @State var viewModel: BuchViewModel
    @State var buch: Buch
    
    @State var neuerBuchTitel = ""
    @State var neuerBuchPreis = ""
    
    @State var rueckmeldung = ""
    
    var body: some View {
        
        VStack(spacing: 20){
            
            TextField("Buch Titel", text: $neuerBuchTitel)
            TextField("Buch Preis, Bsp: 10.99", text: $neuerBuchPreis)
            
            Text(rueckmeldung)
            
            Button("Buch Details ändern."){
                if neuerBuchTitel.count != 0 , let entpackterBuchPreis = Double(neuerBuchPreis){
                    let geaendertesBuch = Buch(titel: neuerBuchTitel, preis: entpackterBuchPreis)
                    viewModel.datenUpdaten(altesBuch: buch, neuesBuch: geaendertesBuch)
                    rueckmeldung = "Änderungen wurden gespeichert."
                    buch = geaendertesBuch
                }
                else{
                    rueckmeldung = "Ein Fehler ist aufgetreten."
                    neuerBuchTitel = buch.titel
                    neuerBuchPreis = String(buch.preis)
                }
            }
            
        }
        .padding()
        .onAppear(perform: {
            neuerBuchTitel = buch.titel
            neuerBuchPreis = String(buch.preis)
        })
        
    }
}

				
			

Die Unterschiede liegen in zwei Dingen. Dem Button und der „.onAppear“ Methode. 

Beim Drücken des Buttons wird ein neues Buch-Objekt erstellt. Dieses neue Objekt wird dann dazu verwendet das alte zu überschreiben. Das Überschreiben des alten Buches mit dem Neuen wird durch die Methode „datenUpdaten“ durchgeführt. Daher ist es notwendig, dass der „View“ Zugriff auf das „View-Model“ hat.

Der „View“ erhält ein Buch Objekt, was in der Variable „buch“ gespeichert wird. Nachdem das Buch einmal geändert wurde, muss diese Variable erneuert werden. Dies geschieht in Zeile 25. Der Grund ist folgender. 

Wenn ich nach einem Buch A suche und den Namen zu B ändere, werde ich bei der zweiten Änderung kein Buch A mehr in der Datenbank finden. Daher muss ich dem View sagen, dass das Buch welches bearbeitet werden soll nun das neue Buch B ist. Wenn das nicht geschieht, wird das Buch A nicht gefunden, und somit auch nicht in der Firebase Datenbank geändert. 

Die Methode „onAppear“ in Zeile 36 sorgt dafür, dass der aktuelle Buchtitel und Preis in die Textfelder geladen werden. Dabei muss der Preis als Double Wert in einen String umgewandelt werden.

Das waren so weit alle Teile der App. Wenn ich noch weiter an der App arbeiten würde, könnte ich noch folgendes machen.

Ich könnte den „TabView“ Teilen. Dazu würde ich zwei neuen „unter-Views“ erstellen. Der Erste würde die Liste enthalten, der Zweite den „View“ zum Hinzufügen von neuen Büchern. Des Weiteren könnte ich Teile des „UpdateView“ extrahieren. Daraus könnte ich eine Seite bauen, die zwei Textfelder, ein Text und einen Button hat. Diesen „View“ könnte ich dann wieder verwenden, wenn ich die „Veiws“ zum Bearbeiten und Erstellen von Büchern nutze. 

Die Funktion „buchLoeschen“ gehört eigentlich nicht in einen „View“. Ich müsste einen Weg finden diese ebenfalls im „View-Model“ zu integrieren. 

 

Offene Fragen

Es gibt zwei Dinge, dich ich noch nicht verstanden habe. 

Zuerst habe ich nicht wirklich das Konzept hinter der Struktur „IndexSet“ verstanden. Die Dokumentation dazu hilft mir nicht wirklich weiter. Ich habe in diesem Projekt anhand eines Beispiels aus dem Internet diese Klasse genutzt. Wirklich verstanden, wie es funktioniert und wofür es gedacht war habe ich aber nicht.

Eine weitere offene Frage betrifft das grundlegende Konzept von Firebase als Datenbank. Da Firebase eine cloudbasierte Datenbank zur Verfügung stellt, greifen bei der App, die ich geschrieben habe, alle Nutzer der App auf die gleiche Datenbank zu. Würde die App also im App Store erscheinen, hätten alle Nutzer der App die gleiche Leseliste und jeder könnte diese Liste bearbeiten. Das wäre nicht sehr nützlich.

Sicherlich gibt es dazu eine Lösung. Wie das möglich ist, weiß ich aktuell aber nicht.

 

Mich benachrichtigen, wenn es einen neuen Beitrag gibt.

 
 
Share on whatsapp
WhatsApp
Share on email
Email
Share on linkedin
LinkedIn
Share on facebook
Facebook