Hentikan gelung geometri SwiftUI dengan menghapuskan laluan daripada saiz yang diukur kembali ke bingkai yang menghasilkannya. Untuk tetingkap Mac yang mengandungi label dengan lebar yang sama, kira saiznya di dalam bekas Layout dan bukannya menyimpan ukuran dalam keadaan.
Panduan ini membina panel label kecil di dalam tetingkap aplikasi anda sendiri. Label menggunakan teks setempat sintetik, berkongsi lebar, dan membalut apabila tetingkap mengecil. Contoh lengkap ini menyasarkan macOS 13 atau lebih baharu; bahagian seterusnya menerangkan cara menerapkan disiplin saiz yang sama apabila antara muka anda menyertakan paparan teks AppKit.
Bagaimanakah pengukuran geometri menjadi gelung susun atur?
Susun atur SwiftUI melibatkan cadangan dan respons: induk mencadangkan saiz, dan anak-anaknya melaporkan saiz yang dipilih. Bekas tersuai menyertai rundingan itu melalui ProposedViewSize. Oleh itu, mengukur paparan berulang kali ialah perkara biasa; pengukuran berulang sahaja tidak membuktikan bahawa ada sesuatu yang salah. Lihat dokumentasi ProposedViewSize Apple.
Kebergantungan bermasalah timbul apabila pengukuran mengubah input kepada susun atur yang sedang diukur. Contohnya, label melaporkan lebar selepas padding, tindakan menyimpan lebar itu, kemudian bingkai sekeliling menggunakan nilai yang disimpan berserta padding tambahan. Bingkai yang terhasil mengubah pengukuran seterusnya. Apple menerangkan secara jelas gelung maklum balas daripada pembaca geometri ke bingkai ini dan mengesyorkan penggunaan sistem susun atur untuk jenis aturan sedemikian. Lihat Karang susun atur tersuai dengan SwiftUI.
Pengiraan ringkas menunjukkan sebab semakan kesamaan mungkin tidak membantu. Katakan pelaksanaan ilustrasi mengukur lebar bingkai sebelumnya ditambah 16 mata padding, kemudian menetapkan hasilnya sebagai lebar bingkai seterusnya. Bermula pada 120 mata menghasilkan 136, 152 dan 168 mata. Setiap hasil berbeza daripada nilai sebelumnya, jadi perlindungan daripada menetapkan nilai yang tidak berubah tidak akan menghentikan pertumbuhan itu. Nombor ini menggambarkan kebergantungan tersebut; ini bukan ukuran daripada aplikasi yang sedang berjalan.
Jejaki kebergantungan dalam kod anda sendiri sebelum memilih pembaikan:
- Cari pembaca geometri, pengendali keutamaan atau tindakan perubahan geometri.
- Kenal pasti setiap nilai keadaan yang menerima ukurannya.
- Cari bingkai, padding, pilihan fon atau paparan bersyarat yang bergantung pada nilai tersebut.
- Periksa sama ada pilihan itu mempengaruhi paparan yang diukur atau induknya.
- Gantikan sementara nilai yang diukur dengan pemalar. Jika perubahan berulang berhenti, periksa laluan kebergantungan itu dahulu.
Mengubah cara sesuatu ukuran dihantar tidak semestinya menghapuskan kitaran. Keutamaan boleh membawa saiz yang sama kembali kepada induk yang sama. Menangguhkan penetapan keadaan boleh memindahkan maklum balas ke kemas kini kemudian. Soalan yang berguna ialah sama ada pengukuran seterusnya masih bergantung pada hasil sebelumnya.
Mekanisme saiz yang manakah patut anda gunakan?
Pilih mekanisme berdasarkan keputusan yang perlu anda buat. Mulakan dengan bekas susun atur terbina dalam; Apple menerangkan pelaksanaan Layout tersuai sebagai pilihan untuk tingkah laku yang memerlukan lebih banyak kawalan. Lihat dokumentasi Layout.
| Hasil yang diperlukan | Pendekatan yang disokong | Syarat utama |
|---|---|---|
| Susun label biasa secara menegak | Tindanan terbina dalam | Biarkan anak mengambil bahagian dalam pensaizan biasa. |
| Pilih persembahan lebar atau padat | ViewThatFits |
Sediakan alternatif mengikut susunan keutamaan. |
| Berikan beberapa label lebar sama berdasarkan kandungan | Layout tersuai |
Ukur dan letakkan anak di dalam kaedah susun atur. |
| Bertindak balas terhadap geometri paparan sedia ada | onGeometryChange |
Perhatikan hanya nilai yang diperlukan oleh tindakan. |
| Tentukan saiz paparan AppKit terbenam | NSViewRepresentable.sizeThatFits |
Kembalikan saiz pilihan untuk cadangan tersebut. |
ViewThatFits memilih anak pertama yang saiz idealnya muat pada paksi yang dihadkan. Ia berguna apabila keperluan sebenar anda ialah memilih antara persembahan, seperti susunan mendatar dan pilihan sandaran menegak. Ini boleh menghapuskan pemboleh ubah keadaan lebar terukur yang berasingan. Lihat dokumentasi ViewThatFits Apple.
Bagi panel label dengan lebar sama di bawah, peraturan saiznya dinyatakan dengan jelas: gunakan lebar ideal label yang paling lebar, dengan had pada lebar cadangan terhingga. Kemudian ukur ketinggian setiap label pada lebar yang dipilih itu. Ini membolehkan tetingkap yang lebih sempit menambah ketinggian panel melalui pembalutan tanpa menghantar ukuran geometri melalui keadaan aplikasi.
Bina panel label dengan lebar sama tanpa keadaan geometri
Cipta projek aplikasi macOS dalam Xcode menggunakan SwiftUI, tetapkan sasaran pelaksanaan kepada macOS 13 atau lebih baharu, dan gantikan deklarasi aplikasi dan paparan kandungan yang dijana dengan contoh lengkap satu fail ini. Kekalkan hanya satu deklarasi aplikasi @main dalam sasaran. Program ini telah diperiksa berdasarkan API yang didokumenkan; ia tidak dikompil atau dijalankan di sini kerana persekitaran yang tersedia ialah 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 kaedah susun atur yang diperlukan membahagikan tugas: sizeThatFits melaporkan saiz bekas, manakala placeSubviews meletakkan anak-anaknya. Proksi anak menyokong pengukuran dan peletakan tanpa mengakses paparan asas secara langsung. Kedua-dua kaedah melaksanakan tanggungjawab yang didokumenkan itu. Lihat dokumentasi Layout Apple.
Butiran pelaksanaan yang penting ialah cadangan lebar dan penggunaannya semula. Setiap anak diukur dengan lebar tertentu dan ketinggian yang tidak ditentukan, membolehkan teks menentukan ketinggian yang diperlukan. Peletakan menggunakan lebar sempadan sebenar bekas dan dasar ketinggian yang sama. Bermula pada bounds.minX dan bounds.minY juga mengelakkan andaian bahawa peletakan bermula pada koordinat asal.
Bekas ini mengendalikan koleksi kosong dengan mengembalikan sifar. Jika lebar tidak ditentukan, ia memilih lebar idealnya; jika lebar terhingga, ia mengehadkan lebar ideal itu. Cadangan lebar tidak terhingga juga menghasilkan lebar ideal terhingga bagi label ini. Ini ialah peraturan yang disengajakan untuk panel bersaiz mengikut kandungan ini. Apple mendokumenkan bahawa induk boleh menguji susun atur dengan cadangan sifar, tidak terhingga, tidak ditentukan dan cadangan lain dalam satu laluan susun atur. Lihat sizeThatFits(proposal:subviews:cache:).
Contoh ini tidak mempunyai keadaan saiz terukur, panggilan balik geometri atau penetapan lebar terpapar kembali ke bingkai. Paparan tatal menegaknya menyediakan ruang untuk kandungan yang lebih tinggi selepas teks membalut. Susun atur ini mengandaikan anak seperti label yang diberikan, yang boleh bertindak balas dengan wajar terhadap lebar terhingga. Anak dengan lebar tetap yang melebihi ruang tersedia memerlukan keputusan persembahan berasingan, seperti tatal mendatar atau paparan alternatif.
Bilakah pemerhatian geometri masih sesuai?
Pemerhatian geometri tetap berguna apabila ukuran mendorong respons yang terhad. API onGeometryChange Apple memisahkan transformasi daripada tindakan: transformasi mengekstrak nilai yang boleh dibandingkan, dan tindakan berjalan apabila nilai yang diubah itu berubah. Apple mengesyorkan agar kemas kini aplikasi yang meluas dielakkan kerana geometri boleh kerap berubah. Lihat onGeometryChange(for:of:action:).
Sebagai contoh, tindanan boleh memaparkan sama ada kawasan bekas yang stabil melebihi lebar yang dipilih. Perhatikan kawasan bekas itu dan kembalikan nilai Boolean daripada transformasi. Pastikan tindanan tidak menyumbang kepada saiz kawasan tersebut. Reka bentuk ini mengikuti model transformasi dan tindakan yang didokumenkan sambil menghapuskan kebergantungan saiz yang berputar.
Mengurangkan segi empat tepat kepada nilai Boolean mengehadkan pemberitahuan, tetapi itu sahaja tidak membuktikan kestabilan. Jika bertukar kepada persembahan padat mengubah kawasan yang diukur secukupnya untuk bertukar semula, nilai Boolean itu boleh bertukar tanpa henti. Ukur kawasan yang lebarnya datang daripada susun atur tetingkap sekeliling, atau gunakan ViewThatFits untuk membuat keputusan persembahan dalam susun atur.
Ketersediaan juga bergantung pada beban lebih. Tindakan nilai baharu tunggal menyokong macOS 13 atau lebih baharu apabila dibina dengan SDK yang menyertakannya; beban lebih nilai lama dan baharu memerlukan macOS 15 atau lebih baharu. Contoh lengkap mengelakkan perbezaan ini dengan menggunakan Layout sahaja. Semak dokumentasi perubahan geometri Apple dan beban lebih nilai lama dan baharu sebelum menyalin tandatangan panggilan balik ke dalam sasaran pelaksanaan yang lebih lama.
Bagaimanakah ukuran AppKit dan TextKit patut dimasukkan ke dalam susun atur SwiftUI?
Apabila kandungannya ialah paparan AppKit, gunakan sempadan saiz representable. NSViewRepresentable.sizeThatFits(_:nsView:context:) tersedia pada macOS 13 atau lebih baharu, mengembalikan saiz pilihan untuk sesuatu cadangan, dan boleh dipanggil berkali-kali dengan cadangan berbeza dalam satu laluan. Mengembalikan nil meminta algoritma saiz lalai. Anggap ini sebagai pertanyaan saiz dan bukannya tempat untuk menerbitkan dimensi terukur ke dalam keadaan SwiftUI. Lihat kaedah saiz representable.
SwiftUI mengawal frame dan bounds bagi paparan AppKit yang diwakili. Apple memberi amaran secara jelas agar sifat susun atur tersebut tidak ditetapkan sendiri kerana tindakan itu bercanggah dengan SwiftUI dan menghasilkan tingkah laku yang tidak ditentukan. Konfigurasikan kandungan dalam kaedah kemas kini representable dan laporkan saiz melalui mekanisme saiz yang disokong. Lihat NSViewRepresentable.
Untuk teks, tentukan dahulu enjin susun atur yang digunakan oleh paparan. TextKit 2 menyediakan NSTextLayoutManager.usageBoundsForTextContainer, tersedia pada macOS 12 atau lebih baharu, sebagai ukuran penggunaan bekas teks. Apple mendokumenkan bahawa ia boleh diperhatikan melalui KVO. Pemerhatian itu boleh menyokong permintaan saiz semula, tetapi ia tidak menghapuskan keperluan untuk memastikan kebergantungan lebar dan ketinggian sentiasa konsisten. Lihat usageBoundsForTextContainer.
Bagi pelaksanaan TextKit 1 sedia ada, NSLayoutManager.usedRect(for:) melaporkan kawasan yang diduduki oleh glif yang sudah disusun atur; ia tidak menjana glif atau melaksanakan susun atur. Apabila ukuran memerlukan susun atur yang selesai, panggil ensureLayout(for:) untuk bekas yang berkaitan sebelum membaca segi empat tepat yang digunakan. Kedua-dua API tersedia mulai macOS 10.7. Lihat usedRect(for:) dan ensureLayout(for:).
Jangan akses layoutManager legasi paparan teks TextKit 2 semata-mata untuk mendapatkan ukuran. Apple menerangkan bahawa akses kepada pengurus susun atur legasi boleh menukar paparan teks kepada mod keserasian TextKit 1. Periksa textLayoutManager dahulu dan kekalkan kod ukuran legasi dalam cabang sandaran yang sesuai. Lihat Perkara baharu dalam TextKit dan paparan teks.
Untuk paparan teks yang membalut, jadikan lebar yang dicadangkan sebagai input kepada susun atur teks dan terbitkan ketinggian pilihan daripada konfigurasi itu. Ambil kira inset paparan teks dan padding bekas secara jelas; segi empat tepat dalam koordinat bekas tidak secara automatik menjadi saiz luar paparan. Pastikan kerja UI AppKit kekal dalam pengasingan aktornya. Bab Concurrency dalam Swift menerangkan pelaksanaan aktor utama; memindahkan kerja ke dalam tugas tidak mengubah kebergantungan berputar dalam formula saiz.
Bagaimanakah anda mengesahkan bahawa susun atur kini stabil?
Jalankan contoh dalam Xcode dan ubah saiz tetingkap perlahan-lahan ke kedua-dua arah. Tingkah laku yang dijangka ialah lebar label yang sama, pembalutan pada lebar yang lebih sempit dan tatal menegak apabila kandungan melebihi ketinggian yang kelihatan. Kemudian ulangi dengan struktur paparan sebenar anda selepas membuang kebergantungan keadaan terukur.
Gunakan semakan berikut untuk membezakan kerja susun atur yang dijangka daripada maklum balas yang berterusan:
- Biarkan tetingkap tidak bergerak selepas mengubah saiznya. Periksa sama ada ukuran dan penetapan keadaan terus berubah tanpa perubahan kandungan.
- Uji label sintetik yang lebih panjang dan fon yang lebih besar. Pastikan panel boleh membesar secara menegak tanpa memotong teks.
- Uji koleksi label kosong. Susun atur patut mengembalikan sifar sebelum padding sekeliling dikenakan.
- Periksa cadangan pengukuran dan peletakan. Anak yang membalut patut menerima andaian lebar yang konsisten.
- Jika anda menambah cache kemudian, masukkan input saiz dalam peraturan kesahihannya; jangan gunakan semula ketinggian yang dikira untuk lebar lain.
Beberapa panggilan saiz dalam satu laluan ialah tingkah laku yang didokumenkan. Sasaran praktikalnya ialah hasil yang stabil untuk kandungan dan kekangan yang tidak berubah. Kekalkan ukuran dalam rundingan susun atur apabila ukuran menentukan aturan, dan gunakan pemerhatian geometri untuk respons yang tidak menghasilkan semula dimensi yang diperhatikannya.
Sumber
- Karang susun atur tersuai 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 baharu — Apple
- sizeThatFits(_:nsView:context:) — Apple
- NSViewRepresentable — Apple
- usageBoundsForTextContainer — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- Perkara baharu dalam TextKit dan paparan teks — Apple
- Concurrency — Bahasa Pengaturcaraan Swift — Swift.org