Hentikan loop geometri SwiftUI dengan menghapus jalur dari ukuran yang diukur kembali ke frame yang menghasilkannya. Untuk jendela Mac yang memuat label-label dengan lebar yang sama, hitung ukurannya di dalam kontainer Layout alih-alih menyimpan hasil pengukuran dalam state.
Panduan ini membuat panel label kecil di dalam jendela aplikasi Anda sendiri. Label menggunakan teks lokal sintetis, memiliki lebar yang sama, dan membungkus teks saat jendela menyempit. Contoh lengkap ini menargetkan macOS 13 atau lebih baru; bagian berikutnya menjelaskan cara menerapkan disiplin pengukuran yang sama saat antarmuka Anda menyertakan tampilan teks AppKit.
Bagaimana pengukuran geometri bisa menjadi loop tata letak?
Tata letak SwiftUI melibatkan proposal dan respons: induk mengusulkan ukuran, lalu turunannya melaporkan ukuran yang dipilih. Kontainer khusus berpartisipasi dalam negosiasi itu melalui ProposedViewSize. Karena itu, mengukur tampilan berulang kali adalah hal yang normal; pengukuran berulang saja tidak membuktikan bahwa ada masalah. Lihat dokumentasi ProposedViewSize Apple.
Ketergantungan bermasalah muncul ketika hasil pengukuran mengubah masukan bagi tata letak yang sedang diukur. Misalnya, sebuah label melaporkan lebarnya setelah padding, suatu aksi menyimpan lebar tersebut, lalu frame di sekelilingnya menggunakan nilai yang disimpan ditambah padding lagi. Frame yang dihasilkan mengubah pengukuran berikutnya. Apple secara eksplisit menjelaskan loop umpan balik dari geometry reader ke frame ini dan menyarankan penggunaan sistem tata letak untuk pengaturan semacam ini. Lihat Menyusun tata letak khusus dengan SwiftUI.
Perhitungan sederhana menunjukkan mengapa pemeriksaan kesamaan nilai mungkin tidak membantu. Misalkan implementasi ilustratif mengukur lebar frame sebelumnya ditambah padding 16 poin, lalu menetapkan hasilnya sebagai lebar frame berikutnya. Dimulai dari 120 poin, hasilnya menjadi 136, 152, lalu 168 poin. Setiap hasil berbeda dari nilai sebelumnya, sehingga pemeriksaan untuk mencegah penetapan nilai yang tidak berubah tidak akan menghentikan pertumbuhan. Angka-angka ini menggambarkan ketergantungannya; ini bukan hasil pengukuran dari aplikasi yang sedang berjalan.
Telusuri ketergantungan dalam kode Anda sendiri sebelum memilih perbaikan:
- Temukan geometry reader, penangan preferensi, atau aksi perubahan geometri.
- Identifikasi setiap nilai state yang menerima hasil pengukurannya.
- Cari frame, padding, pilihan font, atau tampilan kondisional yang bergantung pada nilai-nilai tersebut.
- Periksa apakah pilihan itu memengaruhi tampilan yang diukur atau induk-induknya.
- Ganti sementara nilai yang diukur dengan konstanta. Jika perubahan berulang berhenti, periksa jalur ketergantungan itu terlebih dahulu.
Mengubah cara hasil pengukuran diteruskan belum tentu menghilangkan siklus. Preferensi dapat membawa ukuran yang sama kembali ke induk yang sama. Menunda penetapan state dapat memindahkan umpan balik ke pembaruan berikutnya. Pertanyaan yang berguna adalah apakah pengukuran berikutnya masih bergantung pada hasil sebelumnya.
Mekanisme pengukuran mana yang sebaiknya digunakan?
Pilih mekanisme berdasarkan keputusan yang perlu Anda buat. Mulailah dengan kontainer tata letak bawaan; Apple menjelaskan bahwa implementasi Layout khusus adalah jalur untuk perilaku yang memerlukan kendali lebih besar. Lihat dokumentasi Layout.
| Hasil yang diperlukan | Pendekatan yang didukung | Ketentuan utama |
|---|---|---|
| Menata label biasa secara vertikal | Stack bawaan | Biarkan turunan mengikuti pengukuran normal. |
| Memilih tampilan lebar atau ringkas | ViewThatFits |
Sediakan alternatif sesuai urutan prioritas. |
| Memberi beberapa label lebar yang sama berdasarkan konten | Layout khusus |
Ukur dan tempatkan turunan di dalam metode tata letak. |
| Merespons geometri tampilan yang sudah ada | onGeometryChange |
Amati hanya nilai yang diperlukan oleh aksi. |
| Mengukur tampilan AppKit yang disematkan | NSViewRepresentable.sizeThatFits |
Kembalikan ukuran yang disarankan untuk proposal tersebut. |
ViewThatFits memilih turunan pertama yang ukuran idealnya muat pada sumbu-sumbu yang dibatasi. Ini berguna saat kebutuhan sebenarnya adalah memilih di antara beberapa tampilan, seperti susunan horizontal dan alternatif vertikal. Dengan begitu, Anda dapat menghilangkan variabel state lebar terukur yang terpisah. Lihat dokumentasi ViewThatFits Apple.
Untuk panel label dengan lebar yang sama di bawah, aturan pengukurannya dinyatakan dengan jelas: gunakan lebar ideal label yang paling lebar, dibatasi oleh lebar proposal yang bernilai hingga. Lalu ukur tinggi setiap label pada lebar yang dipilih. Dengan demikian, jendela yang lebih sempit dapat menambah tinggi panel karena teks membungkus, tanpa meneruskan hasil pengukuran geometri melalui state aplikasi.
Buat panel label dengan lebar yang sama tanpa state geometri
Buat proyek aplikasi macOS di Xcode menggunakan SwiftUI, tetapkan target deployment ke macOS 13 atau lebih baru, lalu ganti deklarasi app dan content view yang dihasilkan dengan contoh lengkap satu file ini. Pertahankan hanya satu deklarasi app @main di target. Program ini ditinjau berdasarkan API yang terdokumentasi; program ini tidak dikompilasi atau dijalankan di sini karena lingkungan yang tersedia adalah Linux.
import SwiftUI
private struct EqualWidthColumn: Layout {
private let gap: CGFloat = 8
private func columnWidth(
proposal: ProposedViewSize,
subviews: Subviews
) -> CGFloat {
let idealWidth = subviews.reduce(CGFloat.zero) { result, view in
max(result, view.sizeThatFits(.unspecified).width)
}
guard let offeredWidth = proposal.width,
offeredWidth.isFinite else {
return idealWidth
}
return min(idealWidth, max(0, offeredWidth))
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
guard !subviews.isEmpty else { return .zero }
let width = columnWidth(proposal: proposal, subviews: subviews)
let childProposal = ProposedViewSize(width: width, height: nil)
let contentHeight = subviews.reduce(CGFloat.zero) { result, view in
result + view.sizeThatFits(childProposal).height
}
let totalGap = gap * CGFloat(subviews.count - 1)
return CGSize(width: width, height: contentHeight + totalGap)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let childProposal = ProposedViewSize(
width: bounds.width,
height: nil
)
var y = bounds.minY
for view in subviews {
let height = view.sizeThatFits(childProposal).height
view.place(
at: CGPoint(x: bounds.minX, y: y),
anchor: .topLeading,
proposal: childProposal
)
y += height + gap
}
}
}
private struct ContentView: View {
private let labels = [
"Amber tile",
"Blue tile with a longer local label",
"Green tile"
]
var body: some View {
ScrollView(.vertical) {
EqualWidthColumn {
ForEach(labels, id: \.self) { label in
Text(label)
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.12))
)
}
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(minWidth: 220, minHeight: 180)
}
}
@main
struct GeometryLoopDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Dua metode tata letak yang diperlukan membagi tugas: sizeThatFits melaporkan ukuran kontainer, sedangkan placeSubviews menempatkan turunannya. Proxy turunan mendukung pengukuran dan penempatan tanpa mengakses tampilan yang mendasarinya secara langsung. Kedua metode menjalankan tanggung jawab yang terdokumentasi tersebut. Lihat dokumentasi Layout Apple.
Detail implementasi yang penting adalah proposal lebar dan penggunaannya kembali. Setiap turunan diukur dengan lebar tertentu dan tinggi yang tidak ditentukan, sehingga teks dapat menentukan tinggi yang dibutuhkannya. Penempatan menggunakan lebar aktual pada batas kontainer dan kebijakan tinggi yang sama. Memulai dari bounds.minX dan bounds.minY juga menghindari asumsi bahwa penempatan dimulai dari koordinat asal.
Kontainer menangani koleksi kosong dengan mengembalikan nol. Jika lebarnya tidak ditentukan, kontainer memilih lebar idealnya; jika lebarnya bernilai hingga, kontainer membatasi lebar ideal tersebut. Proposal lebar tak hingga juga menghasilkan lebar ideal yang bernilai hingga untuk label-label ini. Ini adalah aturan yang disengaja untuk panel yang ukurannya mengikuti konten. Apple mendokumentasikan bahwa induk dapat menguji tata letak dengan proposal nol, tak hingga, tidak ditentukan, dan lainnya selama satu tahap tata letak. Lihat sizeThatFits(proposal:subviews:cache:).
Contoh ini tidak memiliki state ukuran terukur, callback geometri, ataupun penetapan lebar hasil render kembali ke frame. Tampilan gulir vertikalnya memberi ruang bagi konten yang lebih tinggi akibat pembungkusan teks. Tata letak ini mengasumsikan turunannya seperti label yang disediakan, yang dapat menyesuaikan diri dengan lebar bernilai hingga. Turunan dengan lebar tetap yang melebihi ruang tersedia memerlukan keputusan tampilan tersendiri, seperti pengguliran horizontal atau tampilan alternatif.
Kapan pengamatan geometri masih tepat digunakan?
Pengamatan geometri tetap berguna saat hasil pengukuran mendorong respons yang terbatas. API Apple onGeometryChange memisahkan transformasi dari aksi: transformasi mengambil nilai yang dapat dibandingkan, lalu aksi dijalankan saat nilai hasil transformasi berubah. Apple menyarankan agar pembaruan aplikasi yang luas dihindari karena geometri dapat sering berubah. Lihat onGeometryChange(for:of:action:).
Sebagai contoh, overlay dapat menampilkan apakah wilayah kontainer yang stabil melebihi lebar tertentu. Amati wilayah kontainer itu dan kembalikan nilai Boolean dari transformasi. Pastikan overlay tidak ikut memengaruhi ukuran wilayah. Rancangan ini mengikuti model transformasi-dan-aksi yang terdokumentasi sekaligus menghilangkan ketergantungan pengukuran yang melingkar.
Mengubah persegi panjang menjadi Boolean membatasi notifikasi, tetapi dengan sendirinya tidak menjamin kestabilan. Jika beralih ke tampilan ringkas mengubah wilayah yang diukur hingga menyebabkan tampilan kembali beralih, nilai Boolean dapat berganti tanpa henti. Ukur wilayah yang lebarnya berasal dari tata letak jendela di sekelilingnya, atau gunakan ViewThatFits untuk menentukan tampilan di dalam tata letak.
Ketersediaan juga bergantung pada overload yang digunakan. Aksi dengan satu nilai baru mendukung macOS 13 atau lebih baru jika dibangun menggunakan SDK yang menyertakannya; overload dengan nilai lama dan baru memerlukan macOS 15 atau lebih baru. Contoh lengkap menghindari perbedaan ini dengan hanya menggunakan Layout. Periksa dokumentasi perubahan geometri Apple dan overload nilai lama dan baru sebelum menyalin tanda tangan callback ke target deployment yang lebih lama.
Bagaimana pengukuran AppKit dan TextKit masuk ke tata letak SwiftUI?
Jika kontennya berupa tampilan AppKit, gunakan batas pengukuran pada representable. NSViewRepresentable.sizeThatFits(_:nsView:context:) tersedia di macOS 13 atau lebih baru, mengembalikan ukuran yang disarankan untuk sebuah proposal, dan dapat dipanggil beberapa kali dengan proposal berbeda dalam satu tahap. Mengembalikan nil meminta algoritme pengukuran bawaan. Perlakukan ini sebagai kueri ukuran, bukan tempat untuk menerbitkan ukuran terukur ke dalam state SwiftUI. Lihat metode pengukuran representable.
SwiftUI mengendalikan frame dan bounds pada tampilan AppKit yang direpresentasikan. Apple secara eksplisit memperingatkan agar Anda tidak menetapkan properti tata letak tersebut sendiri karena hal itu bertentangan dengan SwiftUI dan menghasilkan perilaku yang tidak terdefinisi. Konfigurasikan konten di metode pembaruan representable dan laporkan ukuran melalui mekanisme pengukuran yang didukung. Lihat NSViewRepresentable.
Untuk teks, tentukan terlebih dahulu mesin tata letak yang digunakan tampilan. TextKit 2 menyediakan NSTextLayoutManager.usageBoundsForTextContainer, yang tersedia di macOS 12 atau lebih baru, sebagai pengukuran penggunaan kontainer teks. Apple mendokumentasikan bahwa nilai ini dapat diamati melalui KVO. Pengamatan itu dapat mendukung permintaan pengubahan ukuran, tetapi tidak menghilangkan kebutuhan untuk menjaga konsistensi ketergantungan lebar dan tinggi. Lihat usageBoundsForTextContainer.
Untuk implementasi TextKit 1 yang sudah ada, NSLayoutManager.usedRect(for:) melaporkan area yang ditempati glyph yang sudah ditata letaknya; API ini tidak menghasilkan glyph atau melakukan tata letak. Jika pengukuran memerlukan tata letak yang sudah selesai, panggil ensureLayout(for:) untuk kontainer terkait sebelum membaca persegi panjang yang digunakan. Kedua API tersedia sejak macOS 10.7. Lihat usedRect(for:) dan ensureLayout(for:).
Jangan mengakses layoutManager lama pada tampilan teks TextKit 2 hanya untuk mendapatkan pengukuran. Apple menjelaskan bahwa akses ke layout manager lama dapat mengalihkan tampilan teks ke mode kompatibilitas TextKit 1. Periksa textLayoutManager terlebih dahulu dan tempatkan kode pengukuran lama di cabang fallback yang sesuai. Lihat Yang baru di TextKit dan tampilan teks.
Untuk tampilan teks yang membungkus, jadikan lebar yang diusulkan sebagai masukan bagi tata letak teks dan hitung tinggi yang disarankan berdasarkan konfigurasi itu. Perhitungkan inset tampilan teks dan padding kontainer secara eksplisit; persegi panjang dalam koordinat kontainer tidak otomatis sama dengan ukuran tampilan luarnya. Lakukan pekerjaan UI AppKit dalam isolasi aktornya. Bab Concurrency di Swift menjelaskan eksekusi main actor; memindahkan pekerjaan ke dalam task tidak mengubah ketergantungan melingkar dalam rumus pengukuran.
Bagaimana memastikan tata letak kini stabil?
Jalankan contoh di Xcode dan ubah ukuran jendelanya secara perlahan ke dua arah. Perilaku yang diharapkan adalah lebar label yang sama, teks membungkus pada lebar yang lebih sempit, dan pengguliran vertikal saat konten melebihi tinggi yang terlihat. Kemudian ulangi dengan struktur tampilan Anda yang sebenarnya setelah menghapus ketergantungan pada state hasil pengukuran.
Gunakan pemeriksaan ini untuk membedakan kerja tata letak yang wajar dari umpan balik yang masih berlanjut:
- Biarkan jendela tidak bergerak setelah diubah ukurannya. Periksa apakah hasil pengukuran dan penetapan state terus berubah tanpa perubahan konten.
- Uji label sintetis yang lebih panjang dan ukuran font yang lebih besar. Pastikan panel dapat bertambah tinggi tanpa memotong teks.
- Uji koleksi label kosong. Tata letak seharusnya mengembalikan nol sebelum padding di sekelilingnya diterapkan.
- Periksa proposal saat pengukuran dan penempatan. Turunan yang membungkus teks seharusnya menerima asumsi lebar yang konsisten.
- Jika nanti Anda menambahkan caching, sertakan masukan pengukuran dalam aturan validitasnya; jangan gunakan kembali tinggi yang dihitung untuk lebar berbeda.
Beberapa panggilan pengukuran dalam satu tahap adalah perilaku yang terdokumentasi. Sasaran praktisnya adalah hasil yang stabil ketika konten dan batasan tidak berubah. Pertahankan pengukuran di dalam negosiasi tata letak saat pengukuran menentukan susunan, dan gunakan pengamatan geometri untuk respons yang tidak membentuk ulang dimensi yang diamati.
Sumber
- Menyusun tata letak khusus dengan SwiftUI — Apple
- ProposedViewSize — Apple
- Layout — Apple
- ViewThatFits — Apple
- sizeThatFits(proposal:subviews:cache:) — Apple
- onGeometryChange(for:of:action:) — Apple
- onGeometryChange(for:of:action:) — nilai lama dan baru — Apple
- sizeThatFits(_:nsView:context:) — Apple
- NSViewRepresentable — Apple
- usageBoundsForTextContainer — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- Yang baru di TextKit dan tampilan teks — Apple
- Concurrency — Bahasa Pemrograman Swift — Swift.org