Usa un Layout SwiftUI personalizzato per assegnare alla prima cella due colonne, quindi disponi le celle rimanenti nelle righe successive. Questo esempio per macOS ricalcola le colonne quando cambia la larghezza della finestra e determina l’altezza di ogni riga in base al suo contenuto.
L’attività consiste in una piccola tavolozza desktop: un riquadro introduttivo seguito da undici pulsanti numerati. La prima cella è più larga quando possono entrare almeno due colonne. Alle larghezze ridotte, diventa una cella a colonna singola, così il contenuto può continuare in verticale. Tutte le etichette e le selezioni dell’esempio sono fittizie.
Quale contenitore SwiftUI supporta questa disposizione?
La Grid di Apple supporta celle che si estendono su più colonne tramite gridCellColumns(_:) all’interno di un GridRow. Tuttavia, le righe vanno specificate manualmente. Una vista inserita direttamente in Grid, al di fuori di un GridRow, si estende invece sull’intera griglia. Nessuna delle due disposizioni impagina automaticamente una raccolta lineare in righe che dipendono dalla larghezza. Consulta la documentazione di Grid e la documentazione di gridCellColumns(_:) di Apple.
LazyVGrid dispone automaticamente gli elementi nelle righe: colloca i figli nelle colonne e prosegue nelle righe successive. La configurazione delle colonne documentata non espone il meccanismo GridRow per estendere una singola cella su più colonne. Rendere visivamente più largo il primo figlio non gli assegna ufficialmente due colonne. Consulta la documentazione di LazyVGrid di Apple.
Scegli il contenitore in base al comportamento richiesto:
| Requisito | Approccio adatto |
|---|---|
| Righe specificate esplicitamente, con una cella estesa su due colonne | Grid, GridRow e gridCellColumns(2) |
| Righe riempite automaticamente con celle normali | LazyVGrid |
| Righe riempite automaticamente e prima cella estesa su due spazi | Un Layout personalizzato che implementa questa regola di impaginazione |
Per quest’ultimo caso, il punto di estensione supportato da Apple è il protocollo Layout. I suoi due metodi essenziali restituiscono le dimensioni del contenitore e dispongono i suoi figli. L’implementazione seguente è un algoritmo di impaginazione originale basato su quei metodi documentati. Layout — Apple
Definisci le regole di dimensionamento prima di scrivere il layout
Questo esempio usa una larghezza minima di 96 punti per le colonne normali e una spaziatura di 8 punti. Le colonne si espandono in modo uniforme per utilizzare la larghezza disponibile. Il primo figlio occupa due spazi, compresa la spaziatura tra loro; ogni altro figlio ne occupa uno. I figli riempiono le righe nell’ordine in cui compaiono e l’altezza di ogni riga corrisponde a quella del figlio più alto.
Per una larghezza disponibile W, una larghezza minima della colonna m e una spaziatura g, il numero di colonne è max(1, floor((W + g) / (m + g))). La larghezza effettiva di una colonna normale è (W - (columns - 1) × g) / columns. Una cella a due spazi riceve quindi 2 × columnWidth + g punti.
Per esempio, un’area contenuti larga 408 punti può ospitare quattro colonne da 96 punti, separate da tre spaziature da 8 punti. La cella introduttiva è larga 200 punti. Accanto a essa entrano due pulsanti normali, mentre quelli successivi riempiono quattro spazi per riga. Sono calcoli illustrativi basati sulle costanti scelte, non misurazioni di un’app in esecuzione.
SwiftUI può proporre dimensioni non specificate, pari a zero o infinite quando valuta la flessibilità di un layout. Queste proposte richiedono risposte ponderate. Questa implementazione usa fino a quattro colonne per la larghezza ideale, una colonna per la larghezza minima e un numero sufficiente di colonne per disporre tutti i figli su un’unica riga quando viene richiesta la larghezza massima. Restituisce sempre una larghezza finita. Apple spiega il significato di queste proposte in ProposedViewSize.
Implementa il layout in un’app macOS
Crea un’app macOS SwiftUI in Xcode e imposta la destinazione di distribuzione su macOS 13 o successivo, dove è disponibile Layout. Aggiungi un file Swift chiamato WideFirstGrid.swift con il tipo di layout completo riportato di seguito. 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)
)
}
}
}
La stessa funzione di pianificazione guida la misurazione e il posizionamento. Ogni figlio viene misurato alla larghezza assegnata e con un’altezza non specificata, quindi posizionato usando la stessa proposta di dimensioni. In questo modo, il testo che va a capo e i calcoli dell’altezza delle righe restano coerenti. Apple richiede in particolare che i calcoli siano compatibili tra dimensionamento e posizionamento. placeSubviews(in:proposal:subviews:cache:) — Apple
Il posizionamento aggiunge bounds.minX e bounds.minY a ciascuna posizione calcolata. I limiti assegnati dal contenitore potrebbero non iniziare da zero. Ogni figlio riceve una chiamata di posizionamento, che avviene solo all’interno di placeSubviews, come previsto dal contratto di posizionamento di LayoutSubview.
Aggiungi le celle fittizie e le azioni dei pulsanti
Sostituisci il file di ingresso dell’app generato con il codice seguente, lasciando esattamente una dichiarazione @main nel progetto. Insieme a WideFirstGrid.swift, costituisce l’esempio completo. Il testo introduttivo è il primo figlio del layout; ogni iterazione fornisce un pulsante.
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)
}
}
La raccolta numerica fissa assegna a ogni pulsante un identificatore univoco e stabile tramite id: \.self. Il layout calcola la geometria tramite i proxy delle sottoviste; la selezione resta nello stato della vista e cambia solo nell’azione del pulsante. Apple specifica che ForEach richiede elementi identificabili o un identificatore esplicito. ForEach — Apple
La ScrollView permette di visualizzare altre righe. Il layout calcola intenzionalmente la propria altezza naturale, invece di comprimere le righe per adattarle all’altezza proposta. Un’etichetta di testo più alta aumenta l’altezza della sua riga, e le righe successive si spostano di conseguenza verso il basso. I figli più bassi restano allineati in alto nella riga.
Verifica il ridimensionamento, le larghezze ridotte e il ciclo di vita del layout
Compila ed esegui la destinazione macOS, poi ridimensiona la finestra in orizzontale. I risultati attesi riportati di seguito usano la larghezza disponibile per il contenuto della griglia, dopo la spaziatura circostante. Presuppongono una cella introduttiva e undici pulsanti, per un totale di dodici figli.
| Larghezza del contenuto della griglia | Colonne | Larghezza della prima cella | Righe totali |
|---|---|---|---|
| 96 punti | 1 | 96 punti | 12 |
| 200 punti | 2 | 200 punti | 7 |
| 304 punti | 3 | 200 punti | 5 |
| 408 punti | 4 | 200 punti | 4 |
La larghezza minima della finestra implica che il caso da 96 punti richiede un contenitore di prova più stretto o una riduzione temporanea della larghezza minima della finestra. Prova anche un layout vuoto, un layout contenente soltanto la cella introduttiva e un’etichetta introduttiva più lunga. Il contenuto vuoto restituisce dimensioni pari a zero; una sola cella introduttiva può occupare entrambi gli spazi disponibili senza richiedere un altro figlio.
L’aritmetica dell’impaginazione è stata verificata con un calcolo Python deterministico alle quattro larghezze indicate, controllando anche che non vi fossero sovrapposizioni tra gli spazi né colonne oltre i limiti. L’app SwiftUI è stata ispezionata, ma non compilata né eseguita nell’ambiente di ricerca, che non disponeva di un compilatore Swift. Verifica il comportamento visivo in Xcode prima di adattare l’esempio.
SwiftUI può richiamare ripetutamente sizeThatFits con proposte diverse prima di scegliere una dimensione. Mantieni la misurazione priva di modifiche alla selezione, posizionamento dei figli o altre azioni dell’applicazione. Questo esempio usa la cache vuota predefinita e ricalcola il piano, così una larghezza precedente non può lasciare posizioni di riga obsolete. Apple descrive questo comportamento di misurazione ripetuta in sizeThatFits(proposal:subviews:cache:).
Comprendi i limiti prima di estendere il layout
La larghezza minima di 96 punti è una scelta intenzionale per il layout. Se un contenitore offre meno larghezza, il layout restituisce comunque 96 punti e potrebbe fuoriuscire dall’area assegnata. La larghezza minima della finestra impostata nell’esempio evita questa situazione. Per supportare contenitori più stretti, riduci la larghezza minima della colonna e controlla le etichette a quella larghezza.
Una proposta di dimensioni è una richiesta: un figlio può scegliere dimensioni diverse. Le celle dell’esempio sono progettate per accettare la larghezza assegnata. Sostituirle con contenuti che richiedono una larghezza fissa maggiore può causare sovrapposizioni; in tal caso, rivedi le regole di dimensionamento prima di aggiungerli. place(at:anchor:proposal:) — Apple
Questa implementazione usa anche un posizionamento fisico da sinistra a destra e misura ogni figlio. Non implementa un ordinamento dipendente dalla direzione né la creazione differita. Per raccolte molto più grandi, profila il risultato prima di scegliere un altro contenitore. Apple consiglia di passare da una griglia normale a una griglia differita quando la profilazione dimostra un miglioramento delle prestazioni utile; anche con questo cambiamento occorre ripensare il requisito di estensione delle celle. Grid — Apple
Per una piccola tavolozza Mac, la funzione di pianificazione condivisa rende semplice adattare il comportamento: modifica la larghezza minima della colonna o la spaziatura, mantieni coerenti misurazione e posizionamento e conserva l’adattamento a una colonna. La prima cella resta così larga quando lo spazio lo consente, mentre ogni pulsante rimanente passa allo spazio disponibile successivo.
Fonti
- 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