Użyj niestandardowego układu SwiftUI Layout, aby przydzielić pierwszej komórce dwa miejsca na kolumny, a następnie wypełniaj kolejne wiersze pozostałymi komórkami. Ten przykład dla macOS przelicza kolumny przy zmianie szerokości okna i określa wysokość każdego wiersza na podstawie jego zawartości.
Zadanie polega na utworzeniu niewielkiej palety na komputer: jednego kafelka wprowadzającego, po którym następuje jedenaście ponumerowanych przycisków. Pierwszy kafelek jest szerszy, gdy mieszczą się co najmniej dwie kolumny. Przy małych szerokościach staje się kafelkiem jednokolumnowym, dzięki czemu zawartość może być dalej układana pionowo. Wszystkie etykiety i zaznaczenia w przykładzie są sztuczne.
Który kontener SwiftUI obsługuje taki układ?
Kontener Apple Grid obsługuje komórkę zajmującą wiele kolumn za pomocą gridCellColumns(_:) wewnątrz GridRow. Wiersze trzeba jednak utworzyć samodzielnie. Widok umieszczony bezpośrednio w Grid, poza GridRow, rozciąga się na całą siatkę. Żaden z tych układów nie rozmieszcza automatycznie płaskiej kolekcji w wierszach zależnych od szerokości. Zobacz dokumentację Apple dotyczącą Grid i gridCellColumns(_:).
LazyVGrid zapewnia automatyczne wypełnianie wierszy: umieszcza elementy w kolumnach, a następnie przechodzi do kolejnych wierszy. Udokumentowana konfiguracja kolumn nie udostępnia mechanizmu GridRow, który pozwala pojedynczej komórce zajmować wiele kolumn. Samo wizualne poszerzenie pierwszego elementu nie przydziela mu udokumentowanego miejsca w dwóch kolumnach. Zobacz dokumentację Apple dotyczącą LazyVGrid.
Wybierz kontener odpowiedni do potrzebnego zachowania:
| Wymaganie | Odpowiednie rozwiązanie |
|---|---|
| Jawnie określone wiersze z komórką zajmującą dwie kolumny | Grid, GridRow i gridCellColumns(2) |
| Automatycznie wypełniane wiersze ze zwykłymi komórkami | LazyVGrid |
| Automatycznie wypełniane wiersze i pierwsza komórka zajmująca dwa miejsca | Niestandardowy Layout implementujący tę regułę rozmieszczania |
W tym ostatnim przypadku obsługiwanym punktem rozszerzeń Apple jest protokół Layout. Jego dwie podstawowe metody określają rozmiar kontenera i rozmieszczają jego elementy podrzędne. Poniższa implementacja to autorski algorytm rozmieszczania oparty na tych udokumentowanych metodach. Layout — Apple
Określ zasady wymiarowania przed napisaniem układu
W tym przykładzie minimalna szerokość zwykłej kolumny wynosi 96 punktów, a odstęp 8 punktów. Kolumny rozszerzają się równomiernie, aby wykorzystać dostępną szerokość. Pierwszy element zajmuje dwa miejsca, wliczając odstęp między nimi; każdy pozostały element zajmuje jedno. Elementy wypełniają wiersze w dotychczasowej kolejności, a wysokość każdego wiersza odpowiada wysokości jego najwyższego elementu.
Dla dostępnej szerokości W, minimalnej szerokości kolumny m i odstępu g liczba kolumn wynosi max(1, floor((W + g) / (m + g))). Rzeczywista szerokość zwykłej kolumny to (W - (columns - 1) × g) / columns. Kafelek zajmujący dwa miejsca otrzymuje więc szerokość 2 × columnWidth + g punktów.
Na przykład obszar zawartości o szerokości 408 punktów mieści cztery kolumny po 96 punktów, oddzielone trzema odstępami po 8 punktów. Kafelek wprowadzający ma szerokość 200 punktów. Obok niego mieszczą się dwa zwykłe przyciski, a kolejne przyciski wypełniają po cztery miejsca w każdym wierszu. Są to przykładowe obliczenia na podstawie wybranych stałych, a nie pomiary działającej aplikacji.
Podczas badania elastyczności układu SwiftUI może proponować rozmiar nieokreślony, zerowy lub nieskończony. Na takie propozycje trzeba odpowiednio reagować. Ta implementacja używa maksymalnie czterech kolumn do określenia rozmiaru idealnego, jednej kolumny do określenia minimalnego rozmiaru i takiej liczby kolumn, by wszystkie elementy mogły znaleźć się w jednym wierszu, gdy wymagany jest maksymalny rozmiar. Zawsze zwraca skończoną szerokość. Apple wyjaśnia znaczenie tych propozycji w dokumentacji ProposedViewSize.
Zaimplementuj układ w aplikacji macOS
Utwórz w Xcode aplikację SwiftUI dla macOS i ustaw docelową wersję wdrożenia na macOS 13 lub nowszą, w której dostępny jest Layout. Dodaj plik Swift o nazwie WideFirstGrid.swift i umieść w nim następujący kompletny typ układu. 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)
)
}
}
}
Ta sama funkcja planująca odpowiada za pomiary i rozmieszczanie. Każdy element podrzędny jest mierzony przydzieloną mu szerokością i nieokreśloną wysokością, a następnie umieszczany przy użyciu tej samej propozycji rozmiaru. Dzięki temu zawijanie tekstu i obliczenia wysokości wierszy pozostają spójne. Apple wyraźnie wymaga zgodności obliczeń podczas określania rozmiaru i rozmieszczania. placeSubviews(in:proposal:subviews:cache:) — Apple
Podczas rozmieszczania do każdej obliczonej pozycji dodawane są bounds.minX i bounds.minY. Przydzielone granice rodzica nie muszą zaczynać się od zera. Każdy element podrzędny otrzymuje jedno wywołanie rozmieszczające, a wywołania te odbywają się wyłącznie wewnątrz placeSubviews, zgodnie z wymaganiami kontraktu rozmieszczania LayoutSubview.
Dodaj sztuczne kafelki i akcje przycisków
Zastąp wygenerowany plik wejściowy aplikacji poniższym kodem, pozostawiając w projekcie dokładnie jedną deklarację @main. W połączeniu z plikiem WideFirstGrid.swift stanowi on kompletny przykład. Tekst wprowadzający jest pierwszym elementem układu; każda iteracja dodaje jeden przycisk.
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)
}
}
Stała kolekcja liczb nadaje każdemu przyciskowi unikatowy, stabilny identyfikator za pomocą id: \.self. Układ oblicza geometrię przez proxy widoków podrzędnych; zaznaczenie pozostaje w stanie widoku i zmienia się tylko w akcji przycisku. Apple dokumentuje, że ForEach wymaga elementów z identyfikatorem albo jawnie podanego identyfikatora. ForEach — Apple
ScrollView obsługuje dodatkowe wiersze. Układ celowo oblicza naturalną wysokość zawartości zamiast ściskać wiersze, by dopasować je do proponowanej wysokości. Wyższa etykieta tekstowa zwiększa wysokość swojego wiersza, a kolejne wiersze przesuwają się w dół. Niższe elementy pozostają wyrównane do górnej krawędzi tego wiersza.
Sprawdź zmianę rozmiaru, małe szerokości i cykl życia układu
Zbuduj i uruchom aplikację dla macOS, a następnie zmieniaj szerokość okna. Oczekiwane wyniki w poniższej tabeli odnoszą się do dostępnej szerokości zawartości siatki po uwzględnieniu otaczających ją marginesów. Zakładają kafelek wprowadzający i jedenaście przycisków, czyli łącznie dwanaście elementów.
| Szerokość zawartości siatki | Liczba kolumn | Szerokość pierwszej komórki | Łączna liczba wierszy |
|---|---|---|---|
| 96 punktów | 1 | 96 punktów | 12 |
| 200 punktów | 2 | 200 punktów | 7 |
| 304 punkty | 3 | 200 punktów | 5 |
| 408 punktów | 4 | 200 punktów | 4 |
Minimalna szerokość okna oznacza, że przypadek 96 punktów wymaga osobnego wąskiego kontenera testowego albo tymczasowego zmniejszenia minimalnej szerokości okna. Przetestuj również pusty układ, układ zawierający tylko kafelek wprowadzający oraz dłuższą etykietę wprowadzającą. Pusta zawartość zwraca rozmiar zerowy; pojedynczy pierwszy kafelek może zająć oba dostępne miejsca bez potrzeby dodawania kolejnego elementu.
Arytmetykę rozmieszczania sprawdzono za pomocą deterministycznych obliczeń w Pythonie dla czterech podanych wyżej szerokości, w tym testów nakładania się miejsc i przekroczenia liczby kolumn. Aplikację SwiftUI przeanalizowano, ale nie skompilowano ani nie uruchomiono w środowisku badawczym, które nie miało kompilatora Swift. Przed dostosowaniem przykładu sprawdź jego wygląd w Xcode.
SwiftUI może wielokrotnie wywoływać sizeThatFits z różnymi propozycjami, zanim wybierze rozmiar. Nie zmieniaj stanu zaznaczenia ani nie rozmieszczaj elementów podrzędnych i nie wykonuj innych działań aplikacji podczas pomiaru. Ten przykład korzysta z domyślnej pustej pamięci podręcznej i ponownie oblicza plan, więc poprzednia szerokość nie pozostawia nieaktualnych pozycji wierszy. Apple opisuje takie wielokrotne pomiary w dokumentacji sizeThatFits(proposal:subviews:cache:).
Poznaj ograniczenia przed rozszerzeniem rozwiązania
Minimalna szerokość 96 punktów jest celowo przyjętą zasadą układu. Jeśli rodzic zaoferuje mniejszą szerokość, układ nadal zgłosi 96 punktów i może wyjść poza przydzielony obszar. Ustawione minimum szerokości okna zapobiega tej sytuacji. Aby obsługiwać węższe kontenery, zmniejsz minimalną szerokość kolumny i sprawdź etykiety przy tej szerokości.
Propozycja rozmiaru jest żądaniem: element podrzędny może wybrać inny rozmiar. Podane kafelki zaprojektowano tak, by akceptowały przydzieloną im szerokość. Zastąpienie ich zawartością, która wymaga większej stałej szerokości, może spowodować nakładanie się elementów, dlatego przed wprowadzeniem takich elementów należy zmienić zasady wymiarowania. place(at:anchor:proposal:) — Apple
Ta implementacja stosuje też fizyczne rozmieszczanie od lewej do prawej i mierzy każdy element podrzędny. Nie obsługuje kolejności zależnej od kierunku pisma ani leniwego tworzenia widoków. W przypadku znacznie większych kolekcji sprawdź wydajność przed wyborem innego kontenera. Apple zaleca przejście od zwykłej siatki do siatki leniwej, gdy profilowanie wykaże istotną poprawę wydajności; taka zmiana nadal wymaga ponownego rozważenia wymogu rozciągania komórek na wiele kolumn. Grid — Apple
W przypadku niewielkiej palety na Maca wspólna funkcja planująca ułatwia dostosowanie zachowania: zmień minimalną szerokość kolumny lub odstęp, zachowaj spójność pomiarów i rozmieszczania oraz utrzymaj rozwiązanie awaryjne z jedną kolumną. Pierwszy kafelek pozostanie wtedy szeroki, gdy pozwoli na to miejsce, a pozostałe przyciski będą przechodzić do następnych dostępnych miejsc.
Źródła
- 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