Verwende ein benutzerdefiniertes SwiftUI-Layout, um der ersten Zelle zwei Spaltenplätze zu geben, und fülle anschließend die übrigen Zellen über aufeinanderfolgende Zeilen hinweg. Dieses macOS-Beispiel berechnet die Spalten neu, wenn sich die Fensterbreite ändert, und ermittelt die Höhe jeder Zeile anhand ihres Inhalts.
Die Aufgabe ist eine kleine Desktop-Palette: eine einführende Kachel, gefolgt von elf nummerierten Schaltflächen. Die erste Kachel ist breiter, wenn mindestens zwei Spalten Platz finden. Bei schmalen Breiten wird sie zu einer einspaltigen Kachel, damit der Inhalt vertikal weiterlaufen kann. Alle Beschriftungen und Auswahlen im Beispiel sind synthetisch.
Welcher SwiftUI-Container unterstützt diese Anordnung?
Apples Grid unterstützt eine Zelle, die sich über mehrere Spalten erstreckt, mithilfe von gridCellColumns(_:) innerhalb einer GridRow. Die Zeilen gibst du jedoch selbst vor. Eine Ansicht, die direkt innerhalb von Grid und außerhalb einer GridRow platziert wird, erstreckt sich stattdessen über das gesamte Raster. Keine der beiden Anordnungen verteilt eine flache Sammlung automatisch auf von der Breite abhängige Zeilen. Siehe Apples Grid-Dokumentation und Dokumentation zu gridCellColumns(_:).
LazyVGrid füllt Zeilen automatisch: Es platziert untergeordnete Ansichten spaltenweise und fährt in den folgenden Zeilen fort. Die dokumentierte Spaltenkonfiguration bietet keinen GridRow-Mechanismus, mit dem sich eine einzelne Zelle über mehrere Spalten erstrecken kann. Das erste untergeordnete Element visuell breiter zu machen, legt keine dokumentierte Zuweisung von zwei Spalten fest. Siehe Apples LazyVGrid-Dokumentation.
Wähle den Container entsprechend dem gewünschten Verhalten:
| Anforderung | Geeigneter Ansatz |
|---|---|
| Explizite Zeilen mit einer Zelle, die sich über zwei Spalten erstreckt | Grid, GridRow und gridCellColumns(2) |
| Automatisch gefüllte Zeilen mit gewöhnlichen Zellen | LazyVGrid |
| Automatisch gefüllte Zeilen plus eine erste Zelle, die sich über zwei Plätze erstreckt | Ein benutzerdefiniertes Layout, das diese Packregel umsetzt |
Für diesen letzten Fall ist das Protokoll Layout Apples unterstützter Erweiterungspunkt. Seine beiden wesentlichen Methoden geben die Größe des Containers an und platzieren dessen untergeordnete Ansichten. Die folgende Implementierung ist ein eigener Packalgorithmus, der auf diesen dokumentierten Methoden aufbaut. Layout — Apple
Lege die Größenregeln fest, bevor du das Layout schreibst
Dieses Beispiel verwendet eine Mindestbreite von 96 Punkten für gewöhnliche Spalten und einen Abstand von 8 Punkten. Die Spalten werden gleichmäßig erweitert, um die verfügbare Breite zu nutzen. Das erste untergeordnete Element belegt zwei Plätze einschließlich des Abstands zwischen ihnen; jedes weitere belegt einen Platz. Die Elemente füllen die Zeilen in ihrer bestehenden Reihenfolge, und jede Zeile ist so hoch wie ihr höchstes Element.
Bei verfügbarer Breite W, Mindestspaltenbreite m und Abstand g ergibt sich die Spaltenanzahl max(1, floor((W + g) / (m + g))). Die tatsächliche Breite einer gewöhnlichen Spalte beträgt (W - (columns - 1) × g) / columns. Eine Kachel mit zwei Plätzen erhält daher eine Breite von 2 × columnWidth + g Punkten.
Ein Inhaltsbereich mit 408 Punkten bietet beispielsweise Platz für vier 96 Punkte breite Spalten mit drei Abständen von jeweils 8 Punkten. Die einführende Kachel ist 200 Punkte breit. Neben ihr passen zwei gewöhnliche Schaltflächen, und die folgenden Schaltflächen füllen jeweils vier Plätze pro Zeile. Dies sind veranschaulichende Berechnungen anhand der gewählten Konstanten, keine Messwerte einer laufenden App.
SwiftUI kann beim Ermitteln der Flexibilität eines Layouts eine nicht angegebene, nullgroße oder unendliche Größe vorschlagen. Auf diese Vorschläge muss gezielt reagiert werden. Diese Implementierung verwendet für ihre ideale Breite bis zu vier Spalten, für ihre Mindestbreite eine Spalte und bei maximaler Breite genügend Spalten, damit alle Elemente in eine Zeile passen. Sie gibt immer eine endliche Breite zurück. Apple erläutert die Bedeutung dieser Vorschläge in der Dokumentation zu ProposedViewSize.
Implementiere das Layout in einer macOS-App
Erstelle in Xcode eine SwiftUI-macOS-App und lege das Deployment-Ziel auf macOS 13 oder neuer fest, wo Layout verfügbar ist. Füge eine Swift-Datei namens WideFirstGrid.swift mit dem folgenden vollständigen Layout-Typ hinzu. Layout — Apple
import SwiftUI
struct WideFirstGrid: Layout {
let minimumColumnWidth: CGFloat = 96
let gap: CGFloat = 8
struct Plan {
var size: CGSize
var frames: [CGRect]
}
func makePlan(width proposedWidth: CGFloat?, subviews: Subviews) -> Plan {
guard !subviews.isEmpty else {
return Plan(size: .zero, frames: [])
}
let totalSlots = subviews.count + (subviews.count > 1 ? 1 : 0)
let idealColumns = min(4, totalSlots)
let idealWidth = CGFloat(idealColumns) * minimumColumnWidth
+ CGFloat(idealColumns - 1) * gap
let maximumWidth = CGFloat(totalSlots) * minimumColumnWidth
+ CGFloat(totalSlots - 1) * gap
let width: CGFloat
if let proposedWidth {
if proposedWidth.isFinite {
width = max(minimumColumnWidth, proposedWidth)
} else {
width = maximumWidth
}
} else {
width = idealWidth
}
let columns = max(1, Int(floor(
(width + gap) / (minimumColumnWidth + gap)
)))
let columnWidth = (width - CGFloat(columns - 1) * gap)
/ CGFloat(columns)
var frames: [CGRect] = []
var column = 0
var y: CGFloat = 0
var rowHeight: CGFloat = 0
for (index, subview) in subviews.enumerated() {
let span = index == 0 ? min(2, columns) : 1
if column + span > columns {
y += rowHeight + gap
column = 0
rowHeight = 0
}
let cellWidth = CGFloat(span) * columnWidth
+ CGFloat(span - 1) * gap
let measured = subview.sizeThatFits(
ProposedViewSize(width: cellWidth, height: nil)
)
frames.append(CGRect(
x: CGFloat(column) * (columnWidth + gap),
y: y,
width: cellWidth,
height: measured.height
))
rowHeight = max(rowHeight, measured.height)
column += span
}
return Plan(
size: CGSize(width: width, height: y + rowHeight),
frames: frames
)
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
makePlan(width: proposal.width, subviews: subviews).size
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let plan = makePlan(width: bounds.width, subviews: subviews)
for (subview, frame) in zip(subviews, plan.frames) {
subview.place(
at: CGPoint(
x: bounds.minX + frame.minX,
y: bounds.minY + frame.minY
),
anchor: .topLeading,
proposal: ProposedViewSize(width: frame.width, height: nil)
)
}
}
}
Dieselbe Planungsfunktion steuert Messung und Platzierung. Jedes untergeordnete Element wird mit seiner zugewiesenen Breite und einer nicht angegebenen Höhe gemessen und anschließend mit demselben Größenangebot für dieses Element platziert. So bleiben Textumbrüche und Berechnungen der Zeilenhöhe konsistent. Apple verlangt ausdrücklich kompatible Berechnungen für Größenbestimmung und Platzierung. placeSubviews(in:proposal:subviews:cache:) — Apple
Bei der Platzierung werden bounds.minX und bounds.minY zu jeder berechneten Position addiert. Die zugewiesenen Grenzen des übergeordneten Elements müssen nicht bei null beginnen. Jedes untergeordnete Element erhält genau einen Platzierungsaufruf, und diese Aufrufe erfolgen ausschließlich innerhalb von placeSubviews, wie es der Platzierungsvertrag von LayoutSubview vorschreibt.
Füge die synthetischen Kacheln und Schaltflächenaktionen hinzu
Ersetze die generierte App-Einstiegsdatei durch den folgenden Code und lasse genau eine @main-Deklaration im Projekt. Zusammen mit WideFirstGrid.swift ist dies das vollständige Beispiel. Der einführende Text ist das erste untergeordnete Layout-Element; jede Iteration liefert eine Schaltfläche.
import SwiftUI
@main
struct WideFirstGridDemoApp: App {
var body: some Scene {
WindowGroup {
PaletteView()
.frame(minWidth: 240, minHeight: 260)
}
}
}
struct PaletteView: View {
@State private var selectedNumber: Int? = nil
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Tile palette")
.font(.title2)
Text(selectedNumber.map { "Selected tile: \($0)" }
?? "Choose a numbered tile")
ScrollView {
WideFirstGrid {
Text("Choose a tile from this sample collection")
.font(.headline)
.padding(12)
.frame(
maxWidth: .infinity,
minHeight: 64,
alignment: .leading
)
.background(
Color.blue.opacity(0.12),
in: RoundedRectangle(cornerRadius: 10)
)
ForEach(1...11, id: \.self) { number in
Button {
selectedNumber = number
} label: {
Text("Tile \(number)")
.padding(12)
.frame(maxWidth: .infinity, minHeight: 64)
.background(
Color.gray.opacity(0.12),
in: RoundedRectangle(cornerRadius: 10)
)
}
.buttonStyle(.plain)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(16)
}
}
Die feste numerische Sammlung gibt jeder Schaltfläche über id: \.self eine eindeutige, stabile Kennung. Das Layout berechnet Geometrie mithilfe von Unteransichts-Proxys; die Auswahl bleibt im Zustand der Ansicht und ändert sich nur in der Schaltflächenaktion. Apple dokumentiert, dass ForEach identifizierbare Elemente oder eine explizite Kennung benötigt. ForEach — Apple
Die ScrollView nimmt zusätzliche Zeilen auf. Das Layout berechnet bewusst seine natürliche Inhaltshöhe, statt die Zeilen so zu verkleinern, dass sie in die vorgeschlagene Höhe passen. Ein höheres Textetikett vergrößert die Höhe seiner Zeile, und die folgenden Zeilen rücken entsprechend nach unten. Kürzere Elemente bleiben am oberen Rand dieser Zeile ausgerichtet.
Prüfe Größenänderungen, schmale Breiten und den Layout-Lebenszyklus
Erstelle und starte das macOS-Ziel und ändere dann die Fensterbreite. Die folgenden erwarteten Ergebnisse beziehen sich auf die verfügbare Inhaltsbreite des Rasters nach dem umgebenden Innenabstand. Sie setzen die einführende Kachel plus elf Schaltflächen voraus, insgesamt also zwölf Elemente.
| Inhaltsbreite des Rasters | Spalten | Breite der ersten Zelle | Zeilen insgesamt |
|---|---|---|---|
| 96 Punkte | 1 | 96 Punkte | 12 |
| 200 Punkte | 2 | 200 Punkte | 7 |
| 304 Punkte | 3 | 200 Punkte | 5 |
| 408 Punkte | 4 | 200 Punkte | 4 |
Aufgrund der Mindestbreite des Fensters muss der Fall mit 96 Punkten in einem separaten schmalen Testcontainer oder mit einer vorübergehend reduzierten Mindestfensterbreite geprüft werden. Teste außerdem ein leeres Layout, ein Layout, das nur die einführende Kachel enthält, und eine längere einführende Beschriftung. Leerer Inhalt ergibt eine Größe von null; eine einzelne erste Kachel kann beide verfügbaren Plätze belegen, ohne dass ein weiteres Element nötig ist.
Die Packarithmetik wurde mit einer deterministischen Python-Berechnung für die vier oben genannten Breiten überprüft, einschließlich Prüfungen auf überlappende Plätze und Spaltenüberläufe. Die SwiftUI-Anwendung wurde in der Forschungsumgebung untersucht, aber nicht kompiliert oder ausgeführt, da dort kein Swift-Compiler verfügbar war. Überprüfe das visuelle Verhalten in Xcode, bevor du das Beispiel anpasst.
SwiftUI kann sizeThatFits wiederholt mit verschiedenen Vorschlägen aufrufen, bevor es eine Größe festlegt. Halte die Messung frei von Änderungen an der Auswahl, der Platzierung untergeordneter Ansichten und anderen Anwendungsaktionen. Dieses Beispiel verwendet den standardmäßig leeren Cache und berechnet seinen Plan neu, sodass eine vorherige Breite keine veralteten Zeilenpositionen hinterlassen kann. Apple beschreibt dieses wiederholte Messverhalten in der Dokumentation zu sizeThatFits(proposal:subviews:cache:).
Verstehe die Grenzen, bevor du das Layout erweiterst
Die Mindestbreite von 96 Punkten ist eine bewusste Layoutregel. Wenn ein übergeordnetes Element weniger Breite anbietet, gibt das Layout weiterhin 96 Punkte an und kann über diese Zuweisung hinausreichen. Die angegebene Mindestfensterbreite vermeidet diese Situation. Um schmalere Container zu unterstützen, senke die Mindestspaltenbreite und prüfe die Beschriftungen bei dieser Breite.
Ein Größenangebot ist eine Anfrage: Ein untergeordnetes Element kann eine andere Größe wählen. Die bereitgestellten Kacheln sind so ausgelegt, dass sie ihre zugewiesene Breite annehmen. Wenn du sie durch Inhalte ersetzt, die auf einer größeren festen Breite bestehen, kann es zu Überlappungen kommen. Passe daher die Größenregel an, bevor du solche Elemente hinzufügst. place(at:anchor:proposal:) — Apple
Diese Implementierung verwendet außerdem eine physische Platzierung von links nach rechts und misst jedes untergeordnete Element. Sie implementiert weder eine richtungsabhängige Reihenfolge noch eine verzögerte Erstellung. Bei wesentlich größeren Sammlungen solltest du das Ergebnis profilieren, bevor du einen anderen Container auswählst. Apple empfiehlt, von einem regulären Raster zu einem Lazy-Grid zu wechseln, wenn das Profiling einen lohnenswerten Leistungsvorteil nachweist; bei diesem Wechsel muss die Anforderung an die über mehrere Spalten reichende Zelle weiterhin berücksichtigt werden. Grid — Apple
Für eine kleine Mac-Palette lässt sich das Verhalten mithilfe der gemeinsamen Planungsfunktion unkompliziert anpassen: Ändere das Spaltenminimum oder den Abstand, halte Messung und Platzierung konsistent und behalte den Rückfall auf eine Spalte bei. Die erste Kachel bleibt dann breit, sofern es der verfügbare Platz erlaubt, während jede weitere Schaltfläche in den nächsten freien Platz fließt.
Quellen
- Grid — Apple
- gridCellColumns(_:) — Apple
- LazyVGrid — Apple
- Layout — Apple
- ProposedViewSize — Apple
- sizeThatFits(proposal:subviews:cache:) — Apple
- placeSubviews(in:proposal:subviews:cache:) — Apple
- place(at:anchor:proposal:) — Apple
- ForEach — Apple