Bina rajah kurungan bercabang dalam aplikasi Mac dengan mengira pusat nod sekali, meletakkan kad menggunakan Layout SwiftUI tersuai dan melukis penyambung di belakangnya menggunakan Canvas. Paparan tatal dua paksi memastikan rajah kekal mudah dibaca apabila tetingkap lebih kecil daripada kandungannya.
Tutorial ini menghasilkan rajah sintetik tujuh nod dengan label neutral seperti Node 1. Cabang memanjang ke kiri dari akar di sebelah kanan. Tugas khususnya ialah memaparkan kad yang bersambung dan dijarakkan secara konsisten dalam tetingkap macOS yang boleh diubah saiznya, dengan kedudukan dan lukisan dipacu oleh geometri yang sama.
Pilih struktur reka letak yang memastikan kad dan garisan sejajar
Gunakan sasaran penggunaan macOS 13 atau lebih baharu untuk pelaksanaan ini. Apple menyatakan bahawa Layout tersedia mulai macOS 13. Dua kaedah wajibnya melaporkan saiz bekas dan meletakkan subpaparannya. Ini menjadikannya sesuai untuk menyusun kad pada kedudukan yang dikira. Layout
Kekalkan kad nod sebagai paparan SwiftUI biasa. Gunakan Canvas latar belakang hanya untuk garisan penyambung. Apple menerangkan bahawa elemen individu yang dilukis di dalam kanvas tidak menerima interaktiviti atau kebolehcapaian tersendiri, termasuk paparan yang dibekalkan sebagai simbol kanvas. Kad berasingan mengekalkan cara yang mudah untuk menambah kawalan biasa kemudian. Canvas
Pelaksanaan ini mempunyai tiga bahagian: pepohon yang menerangkan cabang, pengiraan geometri yang menghasilkan pusat nod dan sisi, serta paparan yang menggabungkan kad dengan lukisan penyambung. Kedua-dua lapisan pemaparan menerima nilai geometri yang sama. Oleh itu, menukar pepohon tidak akan mengemas kini kad sambil meninggalkan tatasusunan penyambung yang diselenggarakan secara berasingan.
Dasar saiznya sengaja ditetapkan: saiz kad kekal sama apabila saiz tetingkap berubah, manakala ruang pandang di sekelilingnya boleh ditatal. Apple menyatakan bahawa ScrollView menyokong penatalan mendatar, menegak atau kedua-duanya; ia tidak menyediakan zum. ScrollView
Kira kedudukan cabang berdasarkan jarak daun
Mulakan dengan daun, iaitu nod yang tidak mempunyai anak. Tetapkan slot menegak berturutan kepada setiap daun. Kemudian letakkan setiap induk di tengah-tengah antara anak pertama dan terakhirnya. Kedudukan mendatar bergantung pada kedalaman: akar berada di lajur paling kanan dan setiap penambahan kedalaman mengalihkan satu lajur ke kiri.
Untuk contoh ini, gunakan kad berukuran 132 kali 44 mata, jurang mendatar 64 mata dan jurang menegak 28 mata. Ini ialah nilai reka bentuk sebagai ilustrasi. Dengan tiga lajur dan empat daun, kandungan yang terhasil berukuran 524 kali 260 mata sebelum ruang tepi luar.
| Kuantiti | Pengiraan | Hasil |
|---|---|---|
| Jarak lajur | Lebar kad + jurang mendatar | 196 mata |
| Jarak daun | Tinggi kad + jurang menegak | 72 mata |
| Lebar kandungan | 3 × 132 + 2 × 64 | 524 mata |
| Tinggi kandungan | 4 × 44 + 3 × 28 | 260 mata |
Ini memberikan koordinat menegak pusat keempat-empat daun sebanyak 22, 94, 166 dan 238. Induk terdekatnya berada pada 58 dan 202, manakala akar berada pada 130. Koordinat ini berguna sebagai semakan: induk harus berada di tengah-tengah antara pusat anak paling hujungnya, dan setiap kad harus muat di dalam segi empat kandungan yang dilaporkan.
Algoritma di bawah turut menerima kedalaman cabang yang tidak sekata. Daun cetek kekal dalam lajur kedalamannya dan tidak berpindah secara automatik ke lajur paling kiri. Ini ialah peraturan yang disengajakan dalam contoh ini. Tentukan sama ada penjajaran kedalaman atau penjajaran daun lebih sesuai untuk rajah anda sebelum mengubah formula koordinat.
Bina contoh macOS yang lengkap
Gunakan sasaran aplikasi SwiftUI macOS Xcode dengan sasaran penggunaan macOS 13 atau lebih baharu. Gantikan fail entri aplikasi yang dijana dengannya menggunakan tiga blok kod berikut, mengikut turutan, dalam satu fail Swift. Kesemuanya membentuk program lengkap. Buang pengisytiharan aplikasi lama jika ia berada dalam fail lain: Apple mensyaratkan tepat satu titik entri aplikasi. App
Takrifkan pepohon dan geometri
Setiap nod mempunyai pengecam integer dan tatasusunan anak. Pembina geometri melawati anak sebelum mengira pusat induknya, lalu mengumpulkan peletakan nod dan sisi. Pengecam berulang mencetuskan kegagalan prasyarat, bukannya menghasilkan koleksi paparan yang kabur.
import SwiftUI
struct BranchNode {
let id: Int
let children: [BranchNode]
init(_ id: Int, _ children: [BranchNode] = []) {
self.id = id
self.children = children
}
var depth: Int {
(children.map { $0.depth + 1 }.max()) ?? 0
}
}
struct DiagramNode: Identifiable {
let id: Int
let center: CGPoint
}
struct DiagramEdge {
let child: CGPoint
let parent: CGPoint
}
struct DiagramGeometry {
static let cardSize = CGSize(width: 132, height: 44)
static let horizontalGap: CGFloat = 64
static let verticalGap: CGFloat = 28
let nodes: [DiagramNode]
let edges: [DiagramEdge]
let size: CGSize
static func make(root: BranchNode) -> DiagramGeometry {
let maximumDepth = root.depth
var nextLeaf = 0
var nodes: [DiagramNode] = []
var edges: [DiagramEdge] = []
var identifiers = Set<Int>()
func visit(_ node: BranchNode, depth: Int) -> CGPoint {
precondition(
identifiers.insert(node.id).inserted,
"Every node must have a unique identifier."
)
let x = cardSize.width / 2
+ CGFloat(maximumDepth - depth)
* (cardSize.width + horizontalGap)
let childCenters = node.children.map {
visit($0, depth: depth + 1)
}
let y: CGFloat
if let first = childCenters.first,
let last = childCenters.last {
y = (first.y + last.y) / 2
} else {
y = cardSize.height / 2
+ CGFloat(nextLeaf)
* (cardSize.height + verticalGap)
nextLeaf += 1
}
let center = CGPoint(x: x, y: y)
nodes.append(DiagramNode(id: node.id, center: center))
for child in childCenters {
edges.append(DiagramEdge(child: child, parent: center))
}
return center
}
_ = visit(root, depth: 0)
let size = CGSize(
width: CGFloat(maximumDepth + 1) * cardSize.width
+ CGFloat(maximumDepth) * horizontalGap,
height: CGFloat(nextLeaf) * cardSize.height
+ CGFloat(max(0, nextLeaf - 1)) * verticalGap
)
return DiagramGeometry(nodes: nodes, edges: edges, size: size)
}
}
Model ini sentiasa mengandungi akar, jadi pepohon tanpa anak sekali pun menghasilkan satu kad dengan saiz kandungan bukan sifar. Program ini tidak mempunyai kes rajah kosong yang berasingan. Bagi aplikasi yang membenarkan ketiadaan akar, kendalikan keadaan itu secara jelas sebelum membina geometri ini.
Letakkan kad menggunakan nilai reka letak tersuai
Hantarkan pusat setiap kad melalui LayoutValueKey. Corak yang didokumenkan Apple ialah menetapkan nilai dengan layoutValue(key:value:), kemudian membacanya daripada proksi subpaparan menggunakan kunci tersebut. Ini mengaitkan kedudukan dengan kadnya dan tidak bergantung pada indeks tatasusunan selari. LayoutValueKey
struct DiagramCenterKey: LayoutValueKey {
static let defaultValue: CGPoint = .zero
}
struct BranchLayout: Layout {
let contentSize: CGSize
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
contentSize
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let cardProposal = ProposedViewSize(
width: DiagramGeometry.cardSize.width,
height: DiagramGeometry.cardSize.height
)
for subview in subviews {
let center = subview[DiagramCenterKey.self]
subview.place(
at: CGPoint(
x: bounds.minX + center.x,
y: bounds.minY + center.y
),
anchor: .center,
proposal: cardProposal
)
}
}
}
Reka letak ini mengembalikan saiz terhingga yang sama bagi setiap cadangan kerana kandungannya tidak fleksibel. Apple secara jelas menunjukkan bahawa reka letak tidak fleksibel boleh mengabaikan cadangan. SwiftUI boleh memanggil kaedah penentuan saiz beberapa kali dengan cadangan berbeza semasa satu pas reka letak, jadi kaedah ini hanya mengembalikan geometri; ia tidak melakukan kemas kini model. sizeThatFits(proposal:subviews:cache:)
Peletakan menambahkan bounds.minX dan bounds.minY pada setiap pusat setempat. Sempadan yang diberikan dinyatakan dalam ruang koordinat induk, jadi andaian bahawa asalan bernilai sifar akan merosakkan peletakan apabila bekas bergerak. Apple juga mensyaratkan pengiraan saiz dan peletakan menggunakan jarak dan dimensi yang sepadan. placeSubviews(in:proposal:subviews:cache:)
Penggunaan .center menjadikan titik yang diberikan sebagai titik tengah setiap kad. Setiap proksi diletakkan tepat sekali, di dalam kaedah peletakan, seperti yang diwajibkan API peletakan subpaparan. Bingkai kad yang dinyatakan secara jelas dalam blok seterusnya memastikan kad yang dipaparkan sepadan dengan dimensi geometri. place(at:anchor:proposal:)
Lukis penyambung dan paparkan tetingkap
Setiap penyambung bermula pada tepi kanan kad anak dan berakhir pada tepi kiri kad induk. Dua titik perantaraan menghasilkan laluan bersudut tegak melalui bahagian tengah jurang. Path.addLines(_:) menghasilkan segmen garis bersambung daripada tatasusunan titik. addLines(_:)
struct BranchDiagram: View {
let geometry: DiagramGeometry
var body: some View {
BranchLayout(contentSize: geometry.size) {
ForEach(geometry.nodes) { node in
Text("Node \(node.id)")
.font(.body)
.frame(
width: DiagramGeometry.cardSize.width,
height: DiagramGeometry.cardSize.height
)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.12))
)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.secondary, lineWidth: 1)
}
.layoutValue(
key: DiagramCenterKey.self,
value: node.center
)
}
}
.background {
Canvas { context, _ in
var path = Path()
for edge in geometry.edges {
let start = CGPoint(
x: edge.child.x
+ DiagramGeometry.cardSize.width / 2,
y: edge.child.y
)
let end = CGPoint(
x: edge.parent.x
- DiagramGeometry.cardSize.width / 2,
y: edge.parent.y
)
let middleX = (start.x + end.x) / 2
path.addLines([
start,
CGPoint(x: middleX, y: start.y),
CGPoint(x: middleX, y: end.y),
end
])
}
context.stroke(
path,
with: .color(.secondary),
lineWidth: 1.5
)
}
}
}
}
@main
struct BranchDiagramApp: App {
let geometry = DiagramGeometry.make(
root: BranchNode(1, [
BranchNode(2, [BranchNode(4), BranchNode(5)]),
BranchNode(3, [BranchNode(6), BranchNode(7)])
])
)
var body: some Scene {
WindowGroup {
ScrollView([.horizontal, .vertical]) {
BranchDiagram(geometry: geometry)
.padding(24)
}
.frame(minWidth: 360, minHeight: 240)
}
}
}
ForEach menerima nilai nod yang boleh dikenal pasti, selaras dengan syarat Apple bahawa elemennya boleh dikenal pasti atau hujah pengecam dinyatakan secara jelas. Kekalkan pengecam pada nod logik jika pepohon itu kelak boleh disunting. ForEach
Kanvas dilampirkan terus pada latar belakang reka letak, jadi koordinat setempatnya sepadan dengan geometri yang digunakan untuk kad. Ruang tepi luar digunakan selepas rajah dipasang dan mengalihkan kedua-dua lapisan bersama-sama. Bahagian sepunya penyambung adik-beradik bertindih untuk membentuk persimpangan seperti kurungan.
WindowGroup menyediakan hierarki paparan tetingkap dan tingkah laku pengurusan tetingkap macOS standard. Contoh ini menyimpan geometri sintetik yang tidak berubah dalam struktur aplikasinya; ia tidak memasang pemasa atau panggil balik kitar hayat untuk mengubah kedudukan paparan. WindowGroup
Sahkan rajah dan kenal pasti masalah lazim
Aritmetik untuk contoh ini disemak menggunakan pengiraan Python kecil yang boleh diulang: tujuh pusat, enam sisi, segi empat kandungan berukuran 524 kali 260 mata, dan semua kad berada di dalam segi empat itu. Program SwiftUI telah diperiksa tetapi tidak dapat dikompil kerana tiada pengkompil Swift dalam persekitaran penyelidikan.
Apabila membina aplikasi dalam Xcode, periksa hasil yang boleh diperhatikan berikut:
- Empat kad muncul dalam lajur paling kiri, dua di lajur tengah dan satu di sebelah kanan.
- Setiap penyambung menyentuh titik tengah mendatar pada tepi kad yang berkenaan.
- Setiap induk berada secara menegak di antara anak-anak paling hujungnya.
- Saiz kad kekal sama apabila tetingkap diubah saiznya, dan penatalan tersedia apabila diperlukan.
- Menggantikan akar contoh dengan
BranchNode(1)memaparkan satu kad tanpa penyambung.
Jika kedudukan kad dan garisan tersasar, periksa dahulu peletakan pengubah suai. Ruang tepi atau anjakan yang digunakan pada kad sahaja menambah pelarasan koordinat kedua yang tidak dikongsi oleh geometri penyambung. Kekalkan jarak seluruh rajah di luar BranchDiagram, seperti yang ditunjukkan.
Jika kad muncul secara tidak dijangka pada asalan reka letak, pastikan paparan akhirnya mempunyai nilai DiagramCenterKey. Nilai lalai kunci itu ialah .zero; maka ketiadaan tetapan akan meletakkan pusat kad pada asalan. Jika program berhenti pada prasyarat pengecam, berikan pengecam yang berlainan kepada setiap nod sebelum pemaparan.
Dimensi kad tetap sesuai untuk label sintetik pendek dalam contoh ini. Untuk teks yang lebih panjang atau fon yang lebih besar, tambah fasa pengukuran: minta proksi subpaparan memberikan saiznya, kira lebar lajur dan jarak subpepohon berdasarkan ukuran tersebut, kemudian gunakan hasil yang sama untuk titik hujung penyambung. Cadangan saiz sahaja tidak memaksa subpaparan menerima saiz itu. place(at:anchor:proposal:)
Algoritma ini menerangkan pepohon: setiap anak yang dipaparkan tergolong kepada satu induk. Algoritma ini tidak merutekan pautan silang sewenang-wenangnya atau mewakili nod kongsi di bawah berbilang induk. Keperluan sedemikian memerlukan model geometri yang berbeza dan dasar penghalaan yang jelas. Begitu juga, animasi memerlukan geometri penyambung bergerak seiring dengan kedudukan kad; contoh ini menyediakan peletakan statik.
Pastikan satu pengiraan geometri bertanggungjawab terhadap saiz kandungan, pusat kad dan titik hujung sisi. Dengan itu, anda mempunyai tempat khusus untuk melaraskan jarak, memeriksa koordinat dan mengembangkan rajah sambil mengekalkan keselarasan antara reka letak dan lukisan SwiftUI.
Sumber
- Layout — Apple
- Canvas — Apple
- ScrollView — Apple
- App — Apple
- LayoutValueKey — Apple
- sizeThatFits(proposal:subviews:cache:) — Apple
- placeSubviews(in:proposal:subviews:cache:) — Apple
- place(at:anchor:proposal:) — Apple
- addLines(_:) — Apple
- ForEach — Apple
- WindowGroup — Apple