Använd en anpassad SwiftUI-Layout för att ge den första cellen två kolumnplatser och fyll sedan återstående celler över efterföljande rader. Det här macOS-exemplet räknar om kolumnerna när fönstrets bredd ändras och mäter varje rads höjd utifrån dess innehåll.
Uppgiften är en liten skrivbordspalett: en introduktionsruta följd av elva numrerade knappar. Den första rutan är bredare när minst två kolumner får plats. Vid smala bredder blir den en ruta med en kolumn så att innehållet kan fortsätta vertikalt. Alla etiketter och val i exemplet är syntetiska.
Vilken SwiftUI-behållare stöder den här placeringen?
Apples Grid stöder att en cell sträcker sig över flera kolumner med hjälp av gridCellColumns(_:) inuti en GridRow. Du anger dock själv raderna. En vy som placeras direkt i Grid, utanför en GridRow, sträcker sig i stället över hela rutnätet. Inget av dessa upplägg packar automatiskt en platt samling i rader vars antal beror på bredden. Se Apples dokumentation om Grid och dokumentation om gridCellColumns(_:).
LazyVGrid fyller automatiskt rader: den placerar barnvyer över kolumnerna och fortsätter på efterföljande rader. Dess dokumenterade kolumnkonfiguration tillhandahåller inte GridRow-mekanismen för att låta en enskild cell sträcka sig över flera kolumner. Att göra det första barnet visuellt bredare skapar inte en dokumenterad tilldelning av två kolumner. Se Apples dokumentation om LazyVGrid.
Välj behållare utifrån det beteende du behöver:
| Krav | Lämpligt tillvägagångssätt |
|---|---|
| Uttryckliga rader med en cell som sträcker sig över två kolumner | Grid, GridRow och gridCellColumns(2) |
| Automatiskt fyllda rader med vanliga celler | LazyVGrid |
| Automatiskt fyllda rader samt en första cell som sträcker sig över två platser | En anpassad Layout som implementerar den packningsregeln |
I det sista fallet är Apples stödda utökningspunkt protokollet Layout. Dess två grundläggande metoder anger behållarens storlek och placerar dess barnvyer. Implementeringen nedan är en egen packningsalgoritm byggd med dessa dokumenterade metoder. Layout — Apple
Definiera storleksreglerna innan du skriver layouten
Exemplet använder en minsta bredd på 96 punkter för vanliga kolumner och ett mellanrum på 8 punkter. Kolumnerna får samma extra bredd så att tillgängligt utrymme används. Det första barnet upptar två platser, inklusive mellanrummet mellan dem; alla andra barn upptar en plats. Barnen fyller raderna i den ordning de redan har, och varje rad blir lika hög som sitt högsta barn.
För tillgänglig bredd W, minsta kolumnbredd m och mellanrum g är antalet kolumner max(1, floor((W + g) / (m + g))). Den faktiska bredden för en vanlig kolumn är (W - (columns - 1) × g) / columns. En ruta som upptar två platser får därför bredden 2 × columnWidth + g punkter.
Till exempel får en innehållsyta på 408 punkter plats med fyra kolumner på 96 punkter, med tre mellanrum på 8 punkter. Introduktionsrutan blir 200 punkter bred. Två vanliga knappar får plats bredvid den, och efterföljande knappar fyller fyra platser per rad. Det här är beräkningar utifrån de valda konstanterna, inte mätningar från en körande app.
SwiftUI kan föreslå en ospecificerad, nollstor eller oändlig storlek när den undersöker en layouts flexibilitet. Sådana förslag kräver genomtänkta svar. Den här implementeringen använder upp till fyra kolumner för sin ideala bredd, en kolumn för sin minsta bredd och tillräckligt många kolumner för att alla barn ska få plats på en rad när den efterfrågade bredden är maximal. Den returnerar alltid en ändlig bredd. Apple beskriver betydelsen av dessa förslag i ProposedViewSize.
Implementera layouten i en macOS-app
Skapa en SwiftUI-app för macOS i Xcode och ange macOS 13 eller senare som distributionsmål, eftersom Layout är tillgängligt från och med den versionen. Lägg till en Swift-fil med namnet WideFirstGrid.swift som innehåller följande fullständiga layouttyp. 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)
)
}
}
}
Samma planeringsfunktion styr mätning och placering. Varje barn mäts med sin tilldelade bredd och en ospecificerad höjd, och placeras sedan med samma storleksförslag. Det gör att textradbrytningar och beräkningar av radhöjd stämmer överens. Apple kräver uttryckligen att beräkningarna vid storleksbestämning och placering är kompatibla. placeSubviews(in:proposal:subviews:cache:) — Apple
Placeringen lägger till bounds.minX och bounds.minY till varje beräknad position. Behållarens tilldelade gränser behöver inte börja vid noll. Varje barn får ett placeringsanrop, och dessa anrop sker bara inuti placeSubviews, enligt kraven i LayoutSubviews placeringskontrakt.
Lägg till de syntetiska rutorna och knappåtgärderna
Ersätt den genererade appens startfil med följande kod och se till att projektet innehåller exakt en @main-deklaration. Tillsammans med WideFirstGrid.swift utgör detta hela exemplet. Introduktionstexten är layoutens första barn; varje iteration tillför en knapp.
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)
}
}
Den fasta numeriska samlingen ger varje knapp en unik och stabil identifierare med hjälp av id: \.self. Layouten beräknar geometrin via proxyobjekt för undervyer; valet finns kvar i vyns tillstånd och ändras bara i knappens åtgärd. Apple dokumenterar att ForEach kräver element som kan identifieras eller en uttrycklig identifierare. ForEach — Apple
ScrollView ger plats åt fler rader. Layouten beräknar avsiktligt sin naturliga innehållshöjd i stället för att pressa ihop raderna så att de ryms inom den föreslagna höjden. En högre textetikett ökar höjden på sin rad, och efterföljande rader flyttas nedåt. Kortare barn förblir justerade mot överkanten av raden.
Kontrollera storleksändring, smala bredder och layoutens livscykel
Bygg och kör macOS-målet och ändra sedan fönstrets bredd. De förväntade resultaten nedan gäller rutnätets tillgängliga innehållsbredd efter omgivande utfyllnad. De förutsätter introduktionsrutan och elva knappar, alltså tolv barn totalt.
| Rutnätets innehållsbredd | Kolumner | Första cellens bredd | Totalt antal rader |
|---|---|---|---|
| 96 punkter | 1 | 96 punkter | 12 |
| 200 punkter | 2 | 200 punkter | 7 |
| 304 punkter | 3 | 200 punkter | 5 |
| 408 punkter | 4 | 200 punkter | 4 |
Fönstrets minsta bredd innebär att fallet med 96 punkter kräver en separat smal testbehållare eller en tillfällig sänkning av fönstrets minsta bredd. Testa också en tom layout, en layout som bara innehåller introduktionsrutan och en längre introduktionstext. Tomt innehåll ger storleken noll; en ensam första ruta kan uppta båda tillgängliga platserna utan att ett annat barn behövs.
Packningsaritmetiken kontrollerades med en deterministisk Python-beräkning vid de fyra bredderna ovan, inklusive kontroller av överlappande platser och kolumner som går utanför tillgängligt utrymme. SwiftUI-appen granskades men kompilerades eller kördes inte i undersökningsmiljön, där det saknades en Swift-kompilator. Kontrollera det visuella beteendet i Xcode innan du anpassar exemplet.
SwiftUI kan anropa sizeThatFits upprepade gånger med olika förslag innan den bestämmer en storlek. Håll mätningen fri från ändringar av val, placering av barn och andra programåtgärder. Exemplet använder den tomma standardcachen och räknar om sin plan, så en tidigare bredd kan inte lämna kvar inaktuella radpositioner. Apple beskriver detta upprepade mätbeteende i sizeThatFits(proposal:subviews:cache:).
Förstå begränsningarna innan du bygger vidare
Minimigränsen på 96 punkter är ett medvetet layoutbeslut. Om en förälder erbjuder mindre bredd rapporterar layouten ändå 96 punkter och kan sträcka sig utanför det tilldelade utrymmet. Den angivna minsta fönsterbredden undviker den situationen. Om du vill stödja smalare behållare sänker du den minsta kolumnbredden och kontrollerar etiketter vid den bredden.
Ett storleksförslag är en begäran: ett barn kan välja en annan storlek. Rutorna i exemplet är utformade för att acceptera sin tilldelade bredd. Om du ersätter dem med innehåll som kräver en större fast bredd kan överlappning uppstå, så justera storleksreglerna innan du lägger till sådana barn. place(at:anchor:proposal:) — Apple
Den här implementeringen använder också fysisk placering från vänster till höger och mäter varje barn. Den implementerar inte riktningsberoende ordning eller lat skapande. För betydligt större samlingar bör du profilera resultatet innan du väljer en annan behållare. Apple rekommenderar att byta från ett vanligt rutnät till ett lat rutnät när profilering visar en värdefull prestandaförbättring; vid ett sådant byte behöver kravet på celler som sträcker sig över flera kolumner fortfarande beaktas. Grid — Apple
För en liten Mac-palett är det enkelt att anpassa beteendet tack vare den gemensamma planeringsfunktionen: ändra den minsta kolumnbredden eller mellanrummet, håll mätning och placering konsekventa och behåll reservlösningen med en kolumn. Den första rutan förblir då bred när utrymmet tillåter, medan alla återstående knappar flödar till nästa tillgängliga plats.
Källor
- 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