Gebruik een aangepaste SwiftUI Layout om de eerste cel twee kolomslots te geven en vul vervolgens de resterende cellen over opeenvolgende rijen. Dit macOS-voorbeeld berekent de kolommen opnieuw wanneer de breedte van het venster verandert en bepaalt de hoogte van elke rij op basis van de inhoud.
De taak bestaat uit een klein bureaubladpalet: één introductietegel gevolgd door elf genummerde knoppen. De eerste tegel is breder zodra er minstens twee kolommen passen. Bij smalle breedtes wordt het een tegel met één kolom, zodat de inhoud verticaal verder kan lopen. Alle labels en selecties in het voorbeeld zijn synthetisch.
Welke SwiftUI-container ondersteunt deze indeling?
Apple’s Grid ondersteunt een cel die meerdere kolommen beslaat via gridCellColumns(_:) binnen een GridRow. Je geeft de rijen echter zelf op. Een view die rechtstreeks in Grid staat, buiten een GridRow, beslaat juist het hele grid. Geen van beide indelingen verpakt automatisch een vlakke verzameling in rijen die afhangen van de beschikbare breedte. Zie Apple’s Grid-documentatie en documentatie over gridCellColumns(_:).
LazyVGrid vult rijen automatisch: het plaatst kinderen over kolommen en gaat verder op volgende rijen. De gedocumenteerde kolomconfiguratie biedt geen GridRow-mechanisme waarmee een afzonderlijke cel meerdere kolommen kan beslaan. De eerste child visueel breder maken, reserveert niet aantoonbaar twee kolommen. Zie Apple’s LazyVGrid-documentatie.
Kies de container op basis van het gedrag dat je nodig hebt:
| Vereiste | Geschikte aanpak |
|---|---|
| Expliciete rijen met een cel die twee kolommen beslaat | Grid, GridRow en gridCellColumns(2) |
| Automatisch gevulde rijen met gewone cellen | LazyVGrid |
| Automatisch gevulde rijen plus een eerste cel die twee slots beslaat | Een aangepaste Layout die deze verpakkingsregel implementeert |
Voor dit laatste geval is het ondersteunde uitbreidingspunt van Apple het Layout-protocol. De twee essentiële methoden ervan geven de grootte van de container op en plaatsen de kinderen. De onderstaande implementatie is een eigen verpakkingsalgoritme dat op die gedocumenteerde methoden is gebaseerd. Layout — Apple
Bepaal de groottevoorschriften voordat je de layout schrijft
Dit voorbeeld gebruikt een minimale breedte van 96 punten voor een gewone kolom en een tussenruimte van 8 punten. Kolommen worden even breed uitgebreid om de beschikbare breedte te benutten. Het eerste kind neemt twee slots in, inclusief de tussenruimte ertussen; elk ander kind neemt één slot in. Kinderen vullen de rijen in hun bestaande volgorde, en elke rij is zo hoog als het hoogste kind erin.
Bij een beschikbare breedte W, een minimale kolombreedte m en een tussenruimte g is het aantal kolommen max(1, floor((W + g) / (m + g))). De werkelijke breedte van een gewone kolom is (W - (columns - 1) × g) / columns. Een tegel met twee slots krijgt dus 2 × columnWidth + g punten.
Een inhoudsgebied van 408 punten biedt bijvoorbeeld plaats aan vier kolommen van 96 punten, met drie tussenruimtes van 8 punten. De introductietegel is 200 punten breed. Er passen twee gewone knoppen naast en de volgende knoppen vullen vier slots per rij. Dit zijn illustratieve berekeningen op basis van de gekozen constanten, geen metingen van een draaiende app.
SwiftUI kan een niet-opgegeven, nul- of oneindige grootte voorstellen terwijl het de flexibiliteit van een layout onderzoekt. Deze voorstellen vereisen een doordachte reactie. Deze implementatie gebruikt maximaal vier kolommen voor de ideale breedte, één kolom voor de minimale breedte en voldoende kolommen om alle kinderen op één rij te plaatsen wanneer om de maximale breedte wordt gevraagd. De layout retourneert altijd een eindige breedte. Apple legt de betekenis van deze voorstellen uit in ProposedViewSize.
Implementeer de layout in een macOS-app
Maak in Xcode een SwiftUI macOS-app en stel het implementatiedoel in op macOS 13 of hoger, waar Layout beschikbaar is. Voeg een Swift-bestand toe met de naam WideFirstGrid.swift en plaats daarin het volgende volledige layouttype. 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)
)
}
}
}
Dezelfde planningsfunctie stuurt de meting en de plaatsing aan. Elk kind wordt gemeten met de toegewezen breedte en een niet-opgegeven hoogte en vervolgens geplaatst met hetzelfde groottevoorstel. Zo blijven de berekeningen voor tekstomloop en rijhoogte consistent. Apple vereist expliciet dat de berekeningen voor groottebepaling en plaatsing op elkaar aansluiten. placeSubviews(in:proposal:subviews:cache:) — Apple
Bij de plaatsing worden bounds.minX en bounds.minY opgeteld bij elke berekende positie. De toegewezen grenzen van de parent hoeven niet bij nul te beginnen. Elk kind krijgt één plaatsingsaanroep, en die aanroepen vinden alleen plaats binnen placeSubviews, zoals vereist door het plaatsingscontract van LayoutSubview.
Voeg de synthetische tegels en knopacties toe
Vervang het gegenereerde app-entrybestand door de volgende code en laat precies één @main-declaratie in het project staan. Samen met WideFirstGrid.swift vormt dit het volledige voorbeeld. De introductietekst is het eerste layoutkind; elke iteratie levert één knop.
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)
}
}
De vaste numerieke verzameling geeft elke knop via id: \.self een unieke, stabiele identificatie. De layout berekent geometrie via subview-proxy’s; de selectie blijft in de state van de view en verandert alleen in de knopactie. Apple documenteert dat ForEach identificeerbare elementen of een expliciete identificatie vereist. ForEach — Apple
De ScrollView biedt ruimte aan extra rijen. De layout berekent bewust de natuurlijke inhoudshoogte in plaats van de rijen samen te drukken om in de voorgestelde hoogte te passen. Een hoger tekstlabel vergroot de hoogte van de rij, en latere rijen schuiven dienovereenkomstig omlaag. Kortere kinderen blijven bovenaan die rij uitgelijnd.
Controleer het formaat wijzigen, smalle breedtes en de layoutlevenscyclus
Bouw en start het macOS-doel en wijzig vervolgens de breedte van het venster. De verwachte resultaten hieronder zijn gebaseerd op de beschikbare inhoudsbreedte van het grid, na de omringende padding. Ze gaan uit van de introductietegel plus elf knoppen, dus twaalf kinderen in totaal.
| Breedte van gridinhoud | Kolommen | Breedte eerste cel | Totaal aantal rijen |
|---|---|---|---|
| 96 punten | 1 | 96 punten | 12 |
| 200 punten | 2 | 200 punten | 7 |
| 304 punten | 3 | 200 punten | 5 |
| 408 punten | 4 | 200 punten | 4 |
Door de minimale breedte van het venster vereist het geval van 96 punten een aparte smalle testcontainer of een tijdelijk verlaagde minimale vensterbreedte. Test ook een lege layout, een layout met alleen de introductietegel en een langere introductietekst. Lege inhoud levert een grootte van nul op; een enkele eerste tegel kan beide beschikbare slots innemen zonder dat daarvoor een ander kind nodig is.
De verpakkingsberekeningen zijn gecontroleerd met een deterministische Python-berekening voor de vier bovenstaande breedtes, inclusief controles op overlappende slots en het overschrijden van de kolomgrenzen. De SwiftUI-app is geïnspecteerd, maar niet gecompileerd of gestart in de onderzoeksomgeving, omdat daar geen Swift-compiler beschikbaar was. Controleer het visuele gedrag in Xcode voordat je het voorbeeld aanpast.
SwiftUI kan sizeThatFits herhaaldelijk aanroepen met verschillende voorstellen voordat het een grootte kiest. Voer tijdens het meten geen selectieaanpassingen, plaatsingen van kinderen of andere applicatieacties uit. Dit voorbeeld gebruikt de standaard lege cache en berekent het plan opnieuw, zodat een eerdere breedte geen verouderde rijposities kan achterlaten. Apple beschrijft dit herhaalde meetgedrag in sizeThatFits(proposal:subviews:cache:).
Begrijp de grenzen voordat je de layout uitbreidt
De minimale breedte van 96 punten is een bewust gekozen layoutbeleid. Als een parent minder breedte biedt, rapporteert de layout nog steeds 96 punten en kan deze buiten die toewijzing uitsteken. De minimale vensterbreedte in het voorbeeld voorkomt die situatie. Om smallere containers te ondersteunen, verlaag je de minimale kolombreedte en controleer je de labels op die breedte.
Een groottevoorstel is een verzoek: een kind kan een andere grootte kiezen. De tegels in het voorbeeld zijn ontworpen om hun toegewezen breedte te accepteren. Als je ze vervangt door inhoud die een grotere vaste breedte vereist, kan dat overlap veroorzaken. Pas daarom het groottebeleid aan voordat je zulke kinderen toevoegt. place(at:anchor:proposal:) — Apple
Deze implementatie gebruikt ook fysieke plaatsing van links naar rechts en meet elk kind. De volgorde is niet afhankelijk van de leesrichting en kinderen worden niet lazy aangemaakt. Profileer het resultaat voordat je een andere container kiest voor aanzienlijk grotere verzamelingen. Apple raadt aan om van een gewone grid over te stappen op een lazy grid wanneer profiling een noemenswaardige prestatieverbetering aantoont; bij die wijziging moet je nog steeds opnieuw kijken naar de vereiste dat een cel meerdere kolommen beslaat. Grid — Apple
Voor een klein Mac-palet is het gedrag met de gedeelde planningsfunctie eenvoudig aan te passen: wijzig de minimale kolombreedte of de tussenruimte, houd meting en plaatsing consistent en behoud de terugval naar één kolom. De eerste tegel blijft dan breed wanneer daar ruimte voor is, terwijl elke resterende knop naar het volgende beschikbare slot doorstroomt.
Bronnen
- 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