Agar scroll view SwiftUI pas dengan konten pendek, ukur konten di dalamnya lalu atur tinggi viewport ke nilai yang lebih kecil antara hasil pengukuran tersebut dan batas yang dipilih. Konten yang lebih panjang kemudian dapat digulir dalam batas tersebut. Panduan ini menunjukkan cara membuat perilaku tersebut untuk panel label kecil di app Mac Anda sendiri.
Mengapa scroll view pendek menyisakan ruang kosong?
Mulailah dengan membedakan dua dimensi: tinggi konten dan tinggi viewport yang menampilkannya. Apple menjelaskan bahwa ScrollView menampilkan sebagian dari konten dasarnya dan menyesuaikan bagian yang terlihat saat digulir. Sekelompok label pendek tidak dengan sendirinya menentukan tinggi viewport yang Anda inginkan. Dokumentasi ScrollView dari Apple menjelaskan perbedaan ini.
Untuk panel desktop yang ringkas, tetapkan aturan ukuran secara eksplisit: viewportHeight = min(contentHeight, maximumHeight). Ukur konten pada lebar yang benar-benar diterimanya, termasuk padding apa pun yang seharusnya ikut bergulir. Terapkan hasilnya ke scroll view terluar. Swift min(_:_:) mengembalikan nilai yang lebih kecil, sedangkan SwiftUI frame(width:height:alignment:) menetapkan tinggi tetap jika argumen tingginya diberikan. Swift min(_:_:), Frame tetap SwiftUI.
Sebagai ilustrasi, misalkan batas maksimumnya 240 poin. Konten dengan tinggi 86 poin menghasilkan viewport setinggi 86 poin; konten dengan tinggi 410 poin menghasilkan viewport setinggi 240 poin, sementara sisa konten dapat diakses dengan menggulir. Ini adalah masukan ilustratif, bukan hasil pengukuran dari contoh di bawah.
| Perubahan | Yang dikendalikan | Apakah menetapkan tinggi ringkas yang diinginkan? |
|---|---|---|
| Tinggi konten hasil pengukuran, dibatasi lalu diterapkan ke scroll view | Tinggi viewport | Ya, pada tata letak berbatas yang ditunjukkan di bawah |
.scrollBounceBehavior(.basedOnSize) |
Perilaku pantulan relatif terhadap ukuran konten | Tidak |
.fixedSize(horizontal: false, vertical: true) pada label |
Ukuran vertikal ideal konten | Mendukung pengukuran; viewport terluar tetap memerlukan aturan tinggi |
Apple mendokumentasikan .basedOnSize sebagai konfigurasi pantulan, dan fixedSize(horizontal:vertical:) sebagai cara menetapkan ukuran tampilan sesuai ukuran idealnya pada dimensi yang dipilih. Keduanya tidak menetapkan tinggi viewport berdasarkan konten. Perilaku pantulan scroll, Ukuran tetap per sumbu.
Buat panel ringkas dengan tinggi scroll maksimum
Gunakan instalasi Xcode terkini yang SDK macOS-nya menyediakan overload onGeometryChange(for:of:action:) dengan satu nilai. Saat ini Apple mendokumentasikan overload tersebut tersedia mulai macOS 13.0. Contoh lengkap di bawah menargetkan macOS 13.3 atau lebih baru karena juga menggunakan scrollBounceBehavior(_:axes:), yang tersedia mulai macOS 13.3. Callback perubahan geometri, Ketersediaan modifier pantulan.
Buat proyek app macOS menggunakan SwiftUI. Ganti deklarasi app dan tampilan konten yang dibuat secara otomatis dengan program satu file ini, dan pastikan hanya ada satu deklarasi @main di target. Semua label menggunakan string lokal sintetis. Toggle Show more labels beralih antara konten pendek dan panjang.
import SwiftUI
@main
struct CompactLabelsApp: App {
var body: some Scene {
WindowGroup {
CompactLabelsView()
}
}
}
struct CompactLabelsView: View {
@State private var showMore = false
@State private var viewportHeight: CGFloat = 240
private var labels: [String] {
if showMore {
return (1...18).map { number in
"Sample label \(number): a longer description that can wrap when the panel becomes narrower."
}
}
return ["Sample label A", "Sample label B"]
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Show more labels", isOn: $showMore)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 8) {
if labels.isEmpty {
Text("No labels to display")
} else {
ForEach(labels.indices, id: \.self) { index in
Text(labels[index])
}
}
}
.font(.body)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.onGeometryChange(for: CGFloat.self) { proxy in
min(proxy.size.height.rounded(.up), 240)
} action: { newHeight in
viewportHeight = newHeight
}
}
.frame(height: viewportHeight)
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
.border(Color.secondary.opacity(0.4))
Text("The label panel grows up to 240 points.")
.font(.caption)
}
.padding(20)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.frame(minWidth: 300, minHeight: 360)
}
}
Program ini telah diperiksa, tetapi belum dikompilasi atau dijalankan di sini karena lingkungan ini tidak memiliki compiler Swift atau runtime macOS. Build program ini di Xcode dan lakukan pemeriksaan di bawah sebelum memasukkannya ke antarmuka yang sudah ada.
Mengapa pengukuran dilakukan di dalam scroll view
Callback geometri mengamati stack bagian dalam yang sudah diberi padding. Mengukur scroll view terluar akan melaporkan viewport yang sudah Anda tetapkan, sehingga menghasilkan aturan ukuran melingkar alih-alih menemukan tinggi yang dibutuhkan konten.
Transformasi hanya mengembalikan tinggi yang dibulatkan dan dibatasi. Apple menyarankan agar geometri ditransformasikan menjadi nilai yang dibutuhkan dan agar pembaruan luas akibat perubahan geometri yang sering dihindari. Di sini, perubahan posisi gulir tidak berpengaruh pada perhitungan, dan konten yang lebih tinggi daripada batas tetap menghasilkan nilai 240 poin. Panduan perubahan geometri dari Apple.
Pembulatan ke atas mencegah viewport sengaja dibuat sedikit lebih pendek daripada tinggi hasil pengukuran konten. Nilai awal 240 poin menyediakan viewport yang dapat digunakan sebelum hasil pengukuran tersedia; ini adalah nilai awal sementara, bukan tinggi konten hasil pengukuran. Callback kemudian menggantinya.
Mengapa lebar dan urutan modifier berpengaruh
Pertahankan ukuran horizontal yang fleksibel. Contoh ini menggunakan .fixedSize(horizontal: false, vertical: true) agar label dapat memakai lebar yang tersedia sambil mempertahankan tinggi vertikal idealnya. Apple memperingatkan bahwa menetapkan dimensi ideal dapat membuat tampilan melampaui batas induknya; karena itu, menetapkan kedua dimensi bukan pilihan bawaan yang cocok untuk label yang membungkus teks di panel sempit. Dokumentasi fixedSize(horizontal:vertical:) dari Apple.
Padding 12 poin ditempatkan sebelum modifier pengukuran, sehingga ikut dihitung dalam tinggi konten. Border ditempatkan setelah penetapan tinggi terluar dan menandai batas viewport. Susunan ini memudahkan Anda melihat apakah ruang yang tidak terpakai berada di panel gulir atau di jendela sekitarnya.
Periksa konten pendek, teks yang membungkus, dan penyusutan
Ikuti urutan ini untuk memeriksa perilaku pada contoh, lalu ulangi setelah menempatkan panel di dalam kontainer yang benar-benar digunakan app Anda:
- Jalankan dengan
Show more labelsdalam keadaan nonaktif. Border seharusnya mengelilingi dua label beserta padding-nya, dengan keterangan tepat di bawahnya. - Aktifkan
Show more labels. Panel seharusnya membesar hingga batas 240 poin. Gulir untuk memastikan label terakhir tetap dapat dijangkau. - Persempit dan lebarkan jendela. Tinggi label yang teksnya membungkus seharusnya berubah tanpa menghilangkan baris terakhirnya.
- Nonaktifkan toggle lagi. Panel seharusnya kembali menyusut mengikuti tinggi konten pendek.
- Ubah array pendek sementara menjadi
[]. Pesan eksplisitNo labels to displayseharusnya menentukan tinggi konten. - Ulangi dengan gaya teks dan kontrol di sekitarnya yang digunakan panel sebenarnya.
Tampilan saat kosong merupakan pilihan antarmuka yang disengaja: contoh ini mempertahankan pesan dan padding saat array kosong. Jika perilaku yang diinginkan adalah menghapus seluruh panel, hilangkan seluruh scroll view secara kondisional saat tidak ada label. Itu adalah keputusan produk tersendiri, terpisah dari pengukuran panel yang tidak kosong.
Jendela contoh di sekitarnya mempertahankan tinggi minimum. Kode ini mengatur ukuran viewport label; kode ini tidak meminta jendela Mac menyusut setiap kali label dihapus. Ruang apa pun di bawah keterangan merupakan bagian dari tata letak terluar, di luar scroll view berbingkai.
Cari penyebab celah tersisa atau label terpotong
Jika panel tetap tinggi, periksa terlebih dahulu tempat modifier pengukuran dipasang. Modifier seharusnya mengamati stack konten setelah padding internalnya. Lalu periksa apakah tampilan induk memiliki tinggi atau tinggi minimum eksplisit yang menyisakan ruang di sekitar panel. Apple mendokumentasikan bahwa tinggi frame yang diberikan merupakan dimensi tetap, sehingga frame terluar yang terpisah dapat tetap menyisakan ruang meskipun viewport bagian dalam menyusut. Pengaturan ukuran frame SwiftUI.
Jika konten tetap lebih tinggi daripada yang seharusnya, hapus ekspansi vertikal dari subtree yang diukur. Untuk pola label kecil ini, hindari spacer vertikal atau frame konten dengan tinggi minimum yang diturunkan dari viewport. Pilihan tersebut membuat tinggi konten bergantung pada viewport, sehingga mengganggu pengukuran independen yang dibutuhkan aturan ukuran.
Jika padding bawah terpotong, pastikan padding disertakan sebelum pengukuran. Jika teks membungkus secara berbeda setelah diintegrasikan, periksa lebar aktual yang diberikan oleh tampilan induk baru. Tinggi yang diukur pada satu lebar tidak boleh dianggap sebagai tinggi tetap pada lebar lain.
Sesuaikan batas 240 poin dengan antarmuka yang menampung panel. Contoh ini menetapkan tinggi minimum jendela yang cukup untuk kontrol, padding, keterangan, dan panel yang dibatasi. Kontainer yang lebih pendek memerlukan alokasi ruang yang lebih kecil atau tata letak sekitar yang berbeda; batas tetap ini tidak otomatis menghitung ruang yang tersisa di bawah kontrol lain.
Gunakan pendekatan ini untuk sejumlah kecil label yang ditata sekaligus. Apple menjelaskan LazyVStack sebagai tampilan yang membuat item hanya saat diperlukan untuk dirender di layar. Karena itu, jangan berasumsi bahwa mengganti stack bagian dalam dengan lazy stack akan mempertahankan strategi pengukuran seluruh konten yang sama. Untuk koleksi besar, viewport stabil dengan konten lazy biasanya merupakan rancangan yang lebih sesuai. Dokumentasi LazyVStack dari Apple.
Kapan pengukuran AppKit dan TextKit berlaku?
Untuk panel label SwiftUI ini, ukur subtree SwiftUI yang dirender. Pengukuran teks AppKit akan menambahkan konfigurasi tata letak lain yang harus sesuai dengan label yang ditampilkan.
Jika app Anda sudah menggunakan NSScrollView, pertahankan pembedaan yang sama antara dokumen dan viewport. Apple mendefinisikan documentView sebagai tampilan yang digulir, sedangkan contentSize adalah ukuran tampilan konten scroll view. Walaupun namanya demikian, contentSize bukan pengukuran seluruh dokumen. AppKit documentView, AppKit contentSize.
Untuk tata letak TextKit yang sudah ada dan dibuat berdasarkan NSLayoutManager, usedRect(for:) melaporkan persegi panjang batas glyph yang telah ditata dalam kontainer teks; Apple secara eksplisit menyatakan bahwa permintaan nilainya tidak menghasilkan glyph atau menjalankan tata letak. Jika tata letak seluruh kontainer diperlukan, ensureLayout(for:) adalah metode terdokumentasi untuk menjalankan tata letak tersebut terlebih dahulu. TextKit usedRect(for:), TextKit ensureLayout(for:).
Meski begitu, persegi panjang glyph bukan keseluruhan frame tampilan teks. Apple mendokumentasikan ruang tambahan di sekitar kontainer teks melalui textContainerInset, serta padding fragmen baris di dalamnya. Perhitungkan konfigurasi tampilan teks yang sebenarnya alih-alih menyalin tinggi glyph langsung ke viewport gulir. Metode ini menjelaskan jalur NSLayoutManager; metode ini bukan resep ukuran universal untuk setiap komponen teks. AppKit textContainerInset.
Untuk panel label SwiftUI kecil, solusi praktisnya adalah mempertahankan lebar fleksibel, mengukur konten bagian dalam beserta padding, dan membatasi tinggi viewport terluar. Periksa hasilnya pada konten pendek maupun panjang, lalu uji di dalam tata letak induk yang sebenarnya agar batasan di sekitarnya mendukung perilaku ringkas yang sama.
Sumber
- ScrollView — Apple
- min(::) — Apple
- frame(width:height:alignment:) — Apple
- scrollBounceBehavior(_:axes:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- onGeometryChange(for:of:action:) — Apple
- LazyVStack — Apple
- documentView — Apple
- contentSize — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- textContainerInset — Apple