Gunakan Layout SwiftUI tersuai untuk memberikan dua slot lajur kepada sel pertama, kemudian isi sel yang selebihnya merentasi baris berturutan. Contoh macOS ini mengira semula lajurnya apabila lebar tetingkap berubah dan mengukur ketinggian setiap baris berdasarkan kandungannya.
Tugas ini ialah palet desktop kecil: satu jubin pengenalan diikuti sebelas butang bernombor. Jubin pertama lebih lebar apabila sekurang-kurangnya dua lajur muat. Pada lebar yang sempit, jubin itu menjadi jubin satu lajur supaya kandungan boleh terus menegak. Semua label dan pilihan dalam contoh ini adalah sintetik.
Bekas SwiftUI yang manakah menyokong susunan ini?
Grid Apple menyokong sel yang merentasi beberapa lajur melalui gridCellColumns(_:) di dalam GridRow. Walau bagaimanapun, anda perlu membekalkan baris sendiri. Paparan yang diletakkan terus di dalam Grid, di luar GridRow, merentasi seluruh grid. Kedua-dua susunan itu tidak mengisi baris secara automatik daripada koleksi rata berdasarkan lebar. Lihat dokumentasi Grid dan dokumentasi gridCellColumns(_:) Apple.
LazyVGrid mengisi baris secara automatik: ia meletakkan anak merentasi lajur dan meneruskan ke baris seterusnya. Konfigurasi lajur yang didokumenkan tidak menyediakan mekanisme GridRow untuk membolehkan sel individu merentasi beberapa lajur. Menjadikan anak pertama kelihatan lebih lebar tidak menetapkan peruntukan dua lajur yang didokumenkan. Lihat dokumentasi LazyVGrid Apple.
Pilih bekas berdasarkan tingkah laku yang anda perlukan:
| Keperluan | Pendekatan yang sesuai |
|---|---|
| Baris eksplisit dengan sel yang merentasi dua lajur | Grid, GridRow dan gridCellColumns(2) |
| Baris yang diisi secara automatik dengan sel biasa | LazyVGrid |
| Baris yang diisi secara automatik serta sel pertama merentasi dua slot | Layout tersuai yang melaksanakan peraturan pembungkusan itu |
Untuk kes terakhir ini, titik sambungan yang disokong Apple ialah protokol Layout. Dua kaedah utamanya melaporkan saiz bekas dan meletakkan anak-anaknya. Pelaksanaan di bawah ialah algoritma pembungkusan asal yang dibina menggunakan kaedah yang didokumenkan itu. Layout — Apple
Tetapkan peraturan saiz sebelum menulis layout
Contoh ini menggunakan lebar minimum lajur biasa sebanyak 96 titik dan jarak 8 titik. Lajur mengembang sama rata untuk menggunakan lebar yang tersedia. Anak pertama menggunakan dua slot, termasuk jarak di antaranya; setiap anak yang lain menggunakan satu slot. Anak-anak mengisi baris mengikut susunan sedia ada, dan setiap baris setinggi anaknya yang paling tinggi.
Bagi lebar tersedia W, lebar minimum lajur m dan jarak g, bilangan lajur ialah max(1, floor((W + g) / (m + g))). Lebar lajur biasa yang sebenar ialah (W - (columns - 1) × g) / columns. Oleh itu, jubin dua slot mendapat lebar 2 × columnWidth + g titik.
Sebagai contoh, ruang kandungan selebar 408 titik memuatkan empat lajur selebar 96 titik yang dipisahkan oleh tiga jarak 8 titik. Jubin pengenalan selebar 200 titik. Dua butang biasa muat di sebelahnya, dan butang seterusnya mengisi empat slot bagi setiap baris. Ini ialah pengiraan ilustrasi berdasarkan pemalar yang dipilih, bukan ukuran daripada aplikasi yang sedang berjalan.
SwiftUI mungkin mencadangkan saiz tidak ditentukan, sifar atau infiniti ketika meneliti fleksibiliti sesuatu layout. Cadangan ini memerlukan respons yang disengajakan. Pelaksanaan ini menggunakan sehingga empat lajur untuk lebar idealnya, satu lajur untuk lebar minimumnya, dan bilangan lajur yang mencukupi supaya semua anak berkongsi satu baris apabila diminta lebar maksimum. Ia sentiasa mengembalikan lebar terhingga. Apple menerangkan maksud cadangan ini dalam ProposedViewSize.
Laksanakan layout dalam aplikasi macOS
Cipta aplikasi macOS SwiftUI dalam Xcode dan tetapkan sasaran penggunaannya kepada macOS 13 atau lebih baharu, kerana Layout tersedia mulai versi itu. Tambahkan fail Swift bernama WideFirstGrid.swift yang mengandungi jenis 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 perancangan yang sama digunakan untuk pengukuran dan penempatan. Setiap anak diukur pada lebar yang diperuntukkan dengan ketinggian tidak ditentukan, kemudian diletakkan menggunakan cadangan saiz anak yang sama. Ini memastikan teks berbalut dan pengiraan ketinggian baris konsisten. Apple secara khusus menghendaki pengiraan yang serasi antara penentuan saiz dan penempatan. placeSubviews(in:proposal:subviews:cache:) — Apple
Penempatan menambah bounds.minX dan bounds.minY pada setiap kedudukan yang dikira. Sempadan yang diperuntukkan oleh induk tidak semestinya bermula pada sifar. Setiap anak menerima satu panggilan penempatan, dan panggilan tersebut berlaku hanya di dalam placeSubviews, seperti yang dikehendaki oleh kontrak penempatan LayoutSubview.
Tambahkan jubin sintetik dan tindakan butang
Gantikan fail entri aplikasi yang dijana dengan kod berikut, dengan memastikan hanya terdapat satu pengisytiharan @main dalam projek. Bersama WideFirstGrid.swift, ini ialah contoh lengkap. Teks pengenalan ialah anak layout pertama; setiap iterasi membekalkan satu butang.
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)
}
}
Koleksi angka tetap memberikan setiap butang pengecam unik yang stabil melalui id: \.self. Layout mengira geometri melalui proksi subpaparan; pilihan kekal dalam keadaan paparan dan hanya berubah dalam tindakan butang. Apple mendokumenkan bahawa ForEach memerlukan elemen yang boleh dikenal pasti atau pengecam eksplisit. ForEach — Apple
ScrollView menampung baris tambahan. Layout ini sengaja mengira ketinggian kandungan semula jadi dan bukannya memampatkan baris agar muat pada ketinggian yang dicadangkan. Label teks yang lebih tinggi meningkatkan ketinggian barisnya, dan baris seterusnya bergerak ke bawah. Anak yang lebih pendek kekal sejajar di bahagian atas baris itu.
Semak saiz semula, lebar sempit dan kitar hayat layout
Bina dan jalankan sasaran macOS, kemudian ubah saiz tetingkap secara mendatar. Hasil jangkaan berikut menggunakan lebar kandungan yang tersedia untuk grid, selepas padding sekeliling. Angka ini mengandaikan satu jubin pengenalan bersama sebelas butang, iaitu dua belas anak semuanya.
| Lebar kandungan grid | Lajur | Lebar sel pertama | Jumlah baris |
|---|---|---|---|
| 96 titik | 1 | 96 titik | 12 |
| 200 titik | 2 | 200 titik | 7 |
| 304 titik | 3 | 200 titik | 5 |
| 408 titik | 4 | 200 titik | 4 |
Lebar minimum tetingkap bermakna kes 96 titik memerlukan bekas ujian sempit yang berasingan atau pengurangan sementara lebar minimum tetingkap. Uji juga layout kosong, layout yang hanya mengandungi jubin pengenalan, dan label pengenalan yang lebih panjang. Kandungan kosong mengembalikan saiz sifar; jubin pertama sahaja boleh menggunakan kedua-dua slot yang tersedia tanpa memerlukan anak lain.
Aritmetik pembungkusan disemak menggunakan pengiraan Python deterministik pada empat lebar di atas, termasuk semakan untuk slot yang bertindih dan limpahan lajur. Aplikasi SwiftUI telah diperiksa tetapi tidak dikompil atau dijalankan dalam persekitaran penyelidikan, yang tidak mempunyai pengkompil Swift. Sahkan tingkah laku visual dalam Xcode sebelum mengubah suai contoh ini.
SwiftUI mungkin memanggil sizeThatFits berulang kali dengan cadangan yang berbeza sebelum memilih saiz. Pastikan pengukuran tidak mengubah pilihan, meletakkan anak atau melakukan tindakan aplikasi lain. Contoh ini menggunakan cache kosong lalai dan mengira semula pelannya, jadi lebar terdahulu tidak akan meninggalkan kedudukan baris yang lapuk. Apple menerangkan tingkah laku pengukuran berulang ini dalam sizeThatFits(proposal:subviews:cache:).
Fahami batasannya sebelum mengembangkan contoh ini
Minimum 96 titik ialah dasar layout yang disengajakan. Jika induk menawarkan lebar yang lebih kecil, layout tetap melaporkan 96 titik dan mungkin melangkaui peruntukan itu. Lebar minimum tetingkap yang dibekalkan mengelakkan keadaan tersebut. Untuk menyokong bekas yang lebih sempit, kurangkan lebar minimum lajur dan semak label pada lebar itu.
Cadangan saiz ialah permintaan: anak boleh memilih saiz yang berbeza. Jubin yang dibekalkan direka untuk menerima lebar yang diperuntukkan. Menggantikannya dengan kandungan yang berkeras mahu lebar tetap yang lebih besar boleh menyebabkan pertindihan, jadi semak semula dasar saiz sebelum memperkenalkan anak sedemikian. place(at:anchor:proposal:) — Apple
Pelaksanaan ini juga menggunakan penempatan fizikal dari kiri ke kanan dan mengukur setiap anak. Ia tidak melaksanakan susunan yang bergantung pada arah atau penciptaan malas. Untuk koleksi yang jauh lebih besar, profilkan hasilnya sebelum memilih bekas lain. Apple mengesyorkan beralih daripada grid biasa kepada grid malas apabila pemprofilan menunjukkan peningkatan prestasi yang berbaloi; perubahan itu masih memerlukan pertimbangan semula tentang keperluan merentasi lajur. Grid — Apple
Untuk palet Mac yang kecil, fungsi perancangan bersama menjadikan tingkah laku ini mudah diubah suai: tukar minimum lajur atau jarak, kekalkan konsistensi antara pengukuran dan penempatan, serta kekalkan pilihan satu lajur. Jubin pertama akan kekal lebar apabila ruang mengizinkan, sementara setiap butang yang lain mengalir ke slot tersedia seterusnya.
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