SwiftUI in der Praxis – Teil 1

Buttons in Listenzellen einbinden

Herzlich Willkommen zu dieser neuen Artikelreihe hier auf dem Let’s Code-Blog! Mit SwiftUI in der Praxis möchte ich euch einige Best Practices zu SwiftUI mit auf den Weg geben, die mir bei der täglichen Arbeit mit Apples neuem UI-Framework begegnet sind. Dabei handelt es sich sowohl um diverse Tipps und Tricks als auch Möglichkeiten zur Erkennung und Behebung diverser Fehler (denn so schön SwiftUI auch ist, von Perfektion ist es leider noch ein ziemliches Stück entfernt).

In diesem ersten Artikel möchte ich euch eine Möglichkeit präsentieren, wie ihr Buttons (und entsprechend zugehörige Actions) in Zellen einer SwiftUI-Liste implementieren und nutzen könnt. Der Standardweg greift hier nämlich in SwiftUI nicht.

Das Problem

Gegeben ist das folgende Listing. Darin erfolgt die Erstellung zweier Views. ContentView stellt eine Liste mit insgesamt zehn Zellen dar. Sie ist Teil eines Navigation-Stacks, was dazu führt, dass bei Auswahl einer Zelle eine Detail-View gepusht wird, die nochmals den Text der gewählten Zelle darstellt.

Die Zellen wiederum sind innerhalb von CellView definiert. Sie bestehen aus zwei Informationen: Einem anzuzeigenden Text und einem Binding (presentsAlert). Das Binding ist wichtig im Zusammenspiel mit einem Button, der sich auf jeder Zelle befindet. Wählt man diesen Button aus, soll ein Alert eingeblendet werden, der einen statischen Text ausgibt (ich weiß, das mutet nicht sehr originell an, dafür erlaubt es uns dieser überschaubare Code aber, dass wir uns auf das eigentliche Problem konzentrieren können). Das Binding steuert somit die Sichtbarkeit des Alerts. Die zugehörige Source of Truth für das Binding ist als State-Property in ContentView definiert und steht per Default auf false.

struct ContentView: View {
    @State private var presentsAlert = false
    
    var body: some View {
        NavigationView {
            List(0 ..< 10) { value in
                NavigationLink(destination: Text("Zeile \(value)")) {
                    CellView(text: "Zeile \(value)", presentsAlert: self.$presentsAlert)
                }
            }
            .navigationBarTitle(Text("Liste"))
        }
    }
}

struct CellView: View {
    let text: String
    
    @Binding var presentsAlert: Bool
    
    var body: some View {
        HStack {
            Text(text)
            Spacer()
            Button(action: {
                self.presentsAlert.toggle()
            }) {
                Image(systemName: "info.circle.fill")
            }
        }
        .alert(isPresented: $presentsAlert, content: {
            Alert(title: Text("It works!"))
        })
    }
}

Führt man diesen Code aus, wird die Liste korrekt erzeugt und angezeigt. Auch lassen sich die einzelnen Zellen auswählen, um so zur Detailansicht zu gelangen. Doch ein Betätigen des Buttons ist nicht möglich. Stattdessen führt diese Aktion ebenfalls zur Auswahl der Zelle und so zu einem Push auf dem Navigation-Stack, nicht aber zur gewünschten Anzeige des Alerts.

Die Info-Buttons am rechten Rand jeder Zelle lassen sich mit der bisherigen Implementierung nicht nutzen.
Die Info-Buttons am rechten Rand jeder Zelle lassen sich mit der bisherigen Implementierung nicht nutzen.

Die Lösung

Um einen Button in einer Zellenliste einzubinden, braucht es einen kleinen Kniff. Wir dürfen nämlich nicht direkt eine Button-Instanz innerhalb einer Zelle verwenden. Stattdessen rufen wir auf der View, die als Schaltfläche dienen soll (in unserem Fall das Image), den onTapGesture(_:)-Modifier auf.

Innerhalb des Closure-Parameters dieses Modifiers führt man dann alle Befehle auf, die bei Betätigen des Buttons durchgeführt werden sollen (es handelt sich dabei also um jene Implementierung, die bisher im action-Parameter der Button-Instanz von CellView steckte).

Da die Basis für unseren Button das Info-Image ist, muss der Code wie folgt aktualisiert werden (tatsächlich betrifft die Änderung nur die CellView-Structure):

struct CellView: View {
    let text: String
    
    @Binding var presentsAlert: Bool
    
    var body: some View {
        HStack {
            Text(text)
            Spacer()
            Image(systemName: "info.circle.fill")
                .onTapGesture {
                    self.presentsAlert.toggle()
                }
        }
        .alert(isPresented: $presentsAlert, content: {
            Alert(title: Text("It works!"))
        })
    }
}

Die Button-Instanz verschwindet, und stattdessen weist man dem Image den onTapGesture-Modifier zu. Die darin implementierte Logik bleibt dieselbe. Diese kleine Änderung sorgt dafür, dass sich der Button auswählen lässt und der Alert erscheint. Gleichzeitig lassen sich die Zellen selbst ebenfalls wie gewohnt auswählen, um die zugehörige Detail-View einzublenden.

Nutzt man statt einer Button-Instanz den onTapGesture-Modifier auf einer gewünschten View, lassen sich so Schaltflächen in Listenzellen umsetzen.
Nutzt man statt einer Button-Instanz den onTapGesture-Modifier auf einer gewünschten View, lassen sich so Schaltflächen in Listenzellen umsetzen.

Fazit

Zwar finde ich es persönlich etwas schade, dass sich Button-Instanzen nicht direkt als auswählbare Views innerhalb einer Listenzelle verwenden lassen. Dafür gibt es mit dem onTapGesture-Modifier einen praktischen und simplen Workaround, der die Funktionsweise von Button nachbildet und Zellen so deutlich mehr technische Möglichkeiten zur Verfügung stellt.

Euer Thomas


Kommentare

Schreibe einen Kommentar

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