Gunakan Layout SwiftUI khusus untuk memberi sel pertama dua slot kolom, lalu isi sel yang tersisa secara berurutan di baris-baris berikutnya. Contoh macOS ini menghitung ulang kolom saat lebar jendela berubah dan mengukur tinggi setiap baris berdasarkan kontennya.
Tugas ini berupa palet desktop kecil: satu ubin pengantar diikuti sebelas tombol bernomor. Ubin pertama lebih lebar saat sedikitnya dua kolom dapat dimuat. Pada lebar sempit, ubin itu menjadi ubin satu kolom agar konten dapat terus ditampilkan secara vertikal. Semua label dan pilihan dalam contoh ini bersifat sintetis.
Kontainer SwiftUI mana yang mendukung susunan ini?
Grid dari Apple mendukung sel yang membentang beberapa kolom melalui gridCellColumns(_:) di dalam GridRow. Namun, Anda harus menyediakan barisnya sendiri. Tampilan yang ditempatkan langsung di dalam Grid, di luar GridRow, membentang di seluruh grid. Kedua susunan tersebut tidak secara otomatis mengemas kumpulan datar ke dalam baris yang bergantung pada lebar. Lihat dokumentasi Grid dan dokumentasi gridCellColumns(_:) dari Apple.
LazyVGrid mengisi baris secara otomatis: kontainer ini menempatkan elemen anak melintasi kolom lalu berlanjut ke baris berikutnya. Konfigurasi kolom yang didokumentasikan tidak menyediakan mekanisme GridRow untuk membuat satu sel membentang beberapa kolom. Membuat anak pertama tampak lebih lebar tidak menetapkan alokasi dua kolom yang terdokumentasi. Lihat dokumentasi LazyVGrid dari Apple.
Pilih kontainer berdasarkan perilaku yang Anda butuhkan:
| Persyaratan | Pendekatan yang sesuai |
|---|---|
| Baris eksplisit dengan sel yang membentang dua kolom | Grid, GridRow, dan gridCellColumns(2) |
| Baris yang terisi otomatis dengan sel biasa | LazyVGrid |
| Baris yang terisi otomatis ditambah sel pertama yang membentang dua slot | Layout khusus yang menerapkan aturan pengemasan tersebut |
Untuk kasus terakhir, titik ekstensi yang didukung Apple adalah protokol Layout. Dua metode utamanya melaporkan ukuran kontainer dan menempatkan elemen anaknya. Implementasi di bawah ini adalah algoritma pengemasan orisinal yang dibangun berdasarkan metode yang terdokumentasi tersebut. Layout — Apple
Tetapkan aturan ukuran sebelum menulis layout
Contoh ini menggunakan lebar kolom biasa minimum 96 poin dan jarak 8 poin. Kolom melebar secara merata agar menggunakan lebar yang tersedia. Anak pertama memakai dua slot, termasuk jarak di antara keduanya; setiap anak lainnya memakai satu slot. Anak mengisi baris sesuai urutan yang ada, dan tinggi setiap baris mengikuti anak tertingginya.
Untuk lebar yang tersedia W, lebar kolom minimum m, dan jarak g, jumlah kolom adalah max(1, floor((W + g) / (m + g))). Lebar kolom biasa yang sebenarnya adalah (W - (columns - 1) × g) / columns. Karena itu, ubin dua slot mendapat lebar 2 × columnWidth + g poin.
Sebagai contoh, area konten selebar 408 poin dapat memuat empat kolom selebar 96 poin dengan tiga jarak selebar 8 poin. Ubin pengantar lebarnya 200 poin. Dua tombol biasa dapat muat di sampingnya, dan tombol berikutnya mengisi empat slot per baris. Ini adalah perhitungan ilustratif berdasarkan konstanta yang dipilih, bukan hasil pengukuran aplikasi yang sedang berjalan.
SwiftUI dapat mengusulkan ukuran yang tidak ditentukan, nol, atau tak terhingga saat menelaah fleksibilitas sebuah layout. Proposal ini memerlukan respons yang cermat. Implementasi ini menggunakan hingga empat kolom untuk lebar idealnya, satu kolom untuk lebar minimumnya, dan kolom sebanyak yang diperlukan agar semua anak berbagi satu baris saat diminta memberikan lebar maksimum. Implementasi ini selalu mengembalikan lebar yang terbatas. Apple menjelaskan arti proposal ini di ProposedViewSize.
Implementasikan layout di aplikasi macOS
Buat aplikasi macOS SwiftUI di Xcode dan atur target deployment ke macOS 13 atau lebih baru, karena Layout tersedia mulai versi tersebut. Tambahkan file Swift bernama WideFirstGrid.swift yang berisi tipe layout lengkap berikut. 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)
)
}
}
}
Fungsi perencanaan yang sama digunakan untuk pengukuran dan penempatan. Setiap anak diukur pada lebar alokasinya dengan tinggi yang tidak ditentukan, lalu ditempatkan menggunakan proposal ukuran anak yang sama. Dengan demikian, perhitungan teks yang membungkus dan tinggi baris tetap konsisten. Apple secara khusus mewajibkan perhitungan yang kompatibel antara pengukuran ukuran dan penempatan. placeSubviews(in:proposal:subviews:cache:) — Apple
Penempatan menambahkan bounds.minX dan bounds.minY ke setiap posisi yang dihitung. Batas yang dialokasikan oleh induk tidak harus dimulai dari nol. Setiap anak menerima satu pemanggilan penempatan, dan pemanggilan tersebut hanya terjadi di dalam placeSubviews, sesuai ketentuan kontrak penempatan LayoutSubview.
Tambahkan ubin sintetis dan aksi tombol
Ganti file titik masuk aplikasi yang dibuat secara otomatis dengan kode berikut, dan pastikan proyek hanya memiliki satu deklarasi @main. Bersama WideFirstGrid.swift, kode ini adalah contoh lengkapnya. Teks pengantar menjadi anak pertama layout; setiap iterasi menyediakan satu tombol.
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)
}
}
Kumpulan angka tetap memberi setiap tombol pengenal unik yang stabil melalui id: \.self. Layout menghitung geometri melalui proksi subview; pilihan tetap berada dalam state tampilan dan hanya berubah di dalam aksi tombol. Apple mendokumentasikan bahwa ForEach memerlukan elemen yang dapat diidentifikasi atau pengenal eksplisit. ForEach — Apple
ScrollView menampung baris tambahan. Layout ini sengaja menghitung tinggi konten alaminya alih-alih memampatkan baris agar muat dalam tinggi yang diusulkan. Label teks yang lebih tinggi menambah tinggi barisnya, dan baris berikutnya bergeser ke bawah. Anak yang lebih pendek tetap rata di bagian atas baris tersebut.
Periksa pengubahan ukuran, lebar sempit, dan siklus hidup layout
Bangun dan jalankan target macOS, lalu ubah lebar jendela. Hasil yang diharapkan berikut menggunakan lebar konten yang tersedia untuk grid, setelah padding di sekelilingnya. Perhitungan mengasumsikan ada ubin pengantar ditambah sebelas tombol, sehingga totalnya dua belas anak.
| Lebar konten grid | Kolom | Lebar sel pertama | Total baris |
|---|---|---|---|
| 96 poin | 1 | 96 poin | 12 |
| 200 poin | 2 | 200 poin | 7 |
| 304 poin | 3 | 200 poin | 5 |
| 408 poin | 4 | 200 poin | 4 |
Lebar minimum jendela membuat kasus 96 poin memerlukan kontainer uji sempit terpisah atau pengurangan sementara lebar minimum jendela. Uji juga layout kosong, layout yang hanya berisi ubin pengantar, dan label pengantar yang lebih panjang. Konten kosong menghasilkan ukuran nol; satu ubin pertama dapat memakai kedua slot yang tersedia tanpa memerlukan anak lain.
Aritmetika pengemasan diperiksa dengan perhitungan Python deterministik pada keempat lebar di atas, termasuk pemeriksaan tumpang tindih slot dan luapan kolom. Aplikasi SwiftUI diperiksa, tetapi tidak dikompilasi atau dijalankan di lingkungan riset, karena tidak memiliki kompiler Swift. Pastikan perilaku visualnya di Xcode sebelum mengadaptasi contoh ini.
SwiftUI mungkin memanggil sizeThatFits berulang kali dengan proposal berbeda sebelum menentukan ukuran. Pastikan pengukuran tidak mengubah pilihan, menempatkan anak, atau menjalankan aksi aplikasi lainnya. Contoh ini menggunakan cache kosong bawaan dan menghitung ulang rencananya, sehingga lebar sebelumnya tidak meninggalkan posisi baris yang kedaluwarsa. Apple menjelaskan perilaku pengukuran berulang ini di sizeThatFits(proposal:subviews:cache:).
Pahami batasannya sebelum mengembangkan lebih lanjut
Batas minimum 96 poin merupakan kebijakan layout yang disengaja. Jika induk memberikan lebar yang lebih kecil, layout tetap melaporkan 96 poin dan mungkin melampaui alokasi tersebut. Lebar minimum jendela yang diberikan mencegah situasi ini. Untuk mendukung kontainer yang lebih sempit, turunkan lebar minimum kolom dan periksa label pada lebar tersebut.
Proposal ukuran adalah permintaan: anak dapat memilih ukuran yang berbeda. Ubin yang diberikan dirancang agar menerima lebar alokasinya. Menggantinya dengan konten yang menetapkan lebar tetap lebih besar dapat menyebabkan tumpang tindih, jadi perbarui kebijakan ukuran sebelum menambahkan anak seperti itu. place(at:anchor:proposal:) — Apple
Implementasi ini juga menggunakan penempatan fisik dari kiri ke kanan dan mengukur setiap anak. Implementasi ini tidak menangani urutan yang bergantung pada arah atau pembuatan elemen secara malas. Untuk kumpulan yang jauh lebih besar, lakukan pemrofilan hasilnya sebelum memilih kontainer lain. Apple merekomendasikan untuk beralih dari grid biasa ke grid malas saat pemrofilan menunjukkan peningkatan kinerja yang layak; perubahan itu tetap memerlukan peninjauan ulang atas persyaratan rentang kolom. Grid — Apple
Untuk palet Mac kecil, fungsi perencanaan bersama membuat perilaku ini mudah diadaptasi: ubah lebar minimum kolom atau jaraknya, jaga konsistensi pengukuran dan penempatan, serta pertahankan fallback satu kolom. Dengan demikian, ubin pertama tetap lebar saat ruang memungkinkan, sementara setiap tombol lainnya mengalir ke slot berikutnya yang tersedia.
Sumber
- 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