LazyVGrid und LazyHGrid in SwiftUI


Nach der Vorstellung des SwiftUI-Framework suchten Entwickler für einen Ersatz der beliebte und vielseitige UICollectionView. SwiftUI war zwar in der ersten Version vielseitig, einen leichten Weg Objekte in einem Gitter-Layout anzuordnen, gab es aber nicht. Möglich wurde dies erst mit der Einführung der beiden Grid-Steuerelemente LazyVGrid und LazyHGrid. Sie funktioniert zwar anders als eine UICollectionView, führen aber zu einem vergleichbaren Ergebnis. Grafische Objekten können in einem Gitter aus Zeilen und Spalten angeordnet werden.

Ähnlich wie die LazyVStack und LazyHStack Steuerelemente sind auch die Grids lazy, also faul. Das bedeutet, Objekte im Gitter werden erst dann erzeugt, wenn sie tatsächlich auf dem Bildschirm angezeigt werden. Dieses Verhalten gab es in der ersten Version des SwiftUI-Frameworks noch nicht. Objekte wurden immer erzeugt. Bei einer Tabelle mit mehreren hundert Einträgen wurde unter Umständen Arbeitsspeicher angefordert, der gar nicht benötigt wurde.

In diesem Beispiel verwenden wir eine View mit einem VStack und einem Text als Inhalt für die Gitterzellen. Die View soll die maximal zur Verfügung stehende Breite verwenden, ist aber genau 30 Punkte hoch. Die Hintergrundfarbe soll zufällig gewählt werden. Als Parameter erhält die View eine Ganzzahl. Diese soll später als Text angezeigt werden. So ist es einfach, mir einer For-Schleife beliebig viele dieser View zu erzeugen und sie trotzdem zu unterscheiden.

struct GridCellView {

    public var index : Int

    

    var body: some View {

        VStack {

            Text("\(index)")

                .font(.title)

        }

        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 30, maxHeight: 30, alignment: .center)

        .background(Color.random)

    }

}

Die Konfiguration eines LazyVGrid oder eines LazyHGrid beginnt mit einem Array von GridItems. Ein GridItem bestimmt die Ausrichtung und die Abstände der einzelnen Zellen in einem Grid. Die Anzahl der GridItem im Array legt fest, wie viele Zeilen oder Spalten es gibt. Ein Gitter mit drei Einträgen zeigt das folgende Beispiel. Die Breite für jede Spalte wurde auf den festen Wert (fixed) 120 gesetzt. Der Abstand der Zellen untereinander (spacing) soll 10 sein.

private var columns: [GridItem] = [

    GridItem(.fixed(120), spacing: 10),

    GridItem(.fixed(120), spacing: 10),

    GridItem(.fixed(120), spacing: 10)

]

Wie zuvor erwähnt, genügt eine For-Schleife, um ein Grid mit Inhalt zu füllen. Im folgenden Beispiel kommt ein LazyVGrid zum Einsatz. Das Array der GridItem, passend columns benannt, definiert die Spalten der Tabelle. Es gibt drei Spalten, jeweils 120 Punkte breit mit einem Abstand der einzelnen Zellen in einer Zeile von 10 Punkten. Die Eigenschaft spacing des LazyVGrid bestimmt den Abstand er Zeilen untereinander. Mit alignment center wird bestimmt, dass das Grid selbst in der übergeordneten View zentriert ist.

struct GridViewView {

    

    private var columns: [GridItem] = [

        GridItem(.fixed(120), spacing: 10),

        GridItem(.fixed(120), spacing: 10),

        GridItem(.fixed(120), spacing: 10)

    ]

    

    var body: some View {

        LazyVGrid(columns: columns, alignment: .center, spacing: 16 ) {

            ForEach(1...12, id: \.self) { index in

                GridCell(index: index)

            }

        }

    }

}

Stacks Image 115

Eine optimale Ausnutzung des verfügbaren Platzes wird erreicht, wenn die GridItem keine feste Breite bekommen, sondern sich dynamisch anpassen können. Das wird durch eine Konfiguration mit flexible realisiert. Im folgenden Code haben das erste und das letzte GridItem eine feste Breite von 50 Punkten. Das GridItem in der Mitte wird automatisch den verbleibenden Platz für sich beanspruchen. Der Abstand von 10 Punkten untereinander wird weiterhin eingehalten. Werden mehrere GridItem als flexible deklariert, wird der Platz gleichmäßig aufgeteilt.

private var columns: [GridItem] = [

    GridItem(.fixed(50), spacing: 10),

    GridItem(.flexible(), spacing: 10),

    GridItem(.fixed(50), spacing: 10)]

Stacks Image 117

Sollen im Gitter mehr Objekte angezeigt werden, als gleichzeitig in ein Fenster passen, empfiehlt es sich, die GridView in einem ScrollView einzubetten. Ein ScrollView wird automatisch den zur Verfügung stehenden Platz in Anspruch nehmen. In einem ansonsten leeren Fenster nimmt dieser dann das komplette Fenster ein. Der Inhalt der Scrollview wird beginnend vom oberen Rand her angezeigt. In diesem Beispiel wandert die LazyVGrid deshalb automatisch von der Mitte des Fensters nach oben.

struct GridViewView {

   

    private var columns: [GridItem] = [

        GridItem(.fixed(50), spacing: 10),

        GridItem(.flexible(), spacing: 10),

        GridItem(.fixed(50), spacing: 10)

    ]

    

    var body: some View {

        ScrollView {

            LazyVGrid(columns: columns, alignment: .center, spacing: 16 ) {

                ForEach(1...12, id: \.self) { index in

                    GridCell(index: index)

                }

            }

        }

    }

}

Stacks Image 119

Genau wie bei einer List kann ein Grid in Sektionen unterteilt werden. Sie eignen sich beispielsweise als Überschriften für den Inhalt. Der Header einer solchen Section muss nicht zwangsläufig ein Text sein, jede Art von View ist möglich. So wäre es zum Beispiel leicht zu realisieren, eine Überschrift zusammen mit einem Symbol anzuzeigen. Das Grid ist in der Lage, bestimmte untergeordnete Views »anzupinnen«. Im folgenden Beispiel passiert das mit dem sectionHeaders. Die Section-Überschriften bleiben dann am oberen Rand der View »kleben«, wenn der Inhalt der Sektion hinausgescrollt wird.

struct GridViewView {

   

    private var columns: [GridItem] = [

        GridItem(.fixed(50), spacing: 10),

        GridItem(.flexible(), spacing: 10),

        GridItem(.fixed(50), spacing: 10)]

    

    var body: some View {

        ScrollView {

            LazyVGrid(columns: columns, alignment: .center, spacing: 16,

                      pinnedViews: [.sectionHeaders]

            ) {

                Section(header: Text("Tabellenüberschrift 1").font(.title)) {

                    ForEach(1...6, id: \.self) { index in

                        GridCell(index: index)

                    }

                }

                Section(header: Text("Tabellenüberschrift 2").font(.title)) {

                    ForEach(7...12, id: \.self) { index in

                        GridCell(index: index)

                    }

                }

            }.padding(5)

        }

    }

}


Stacks Image 121

GridItem mit flexibler Breite können über die Eigenschaften minimum und maximum noch präziser konfiguriert werden. So lässt sich verhindern, dass ein Objekt zu klein wird. Außerdem lässt sich so das unkontrollierte Wachstum eines Elementes begrenzen, wenn der View plötzlich mehr Platz zur Verfügung steht. Zum Beispiel, wenn ein iOS-Gerät von der Portrait-Ausrichtung um 90 Grad in die Landscape-Ausrichtung gedreht wird.

private var columns: [GridItem] = [

    GridItem(.flexible(minimum: 10, maximum: 400), spacing: 10),

    GridItem(.flexible(minimum: 10, maximum: 400), spacing: 10),

    GridItem(.flexible(minimum: 10, maximum: 400), spacing: 10)]

Stacks Image 123

Geschrieben am: 25.07.2020
Technologien: Swift, SwiftUI, macOS, iOS