नोड केंद्रों की गणना एक बार करके, कस्टम SwiftUI Layout से कार्ड रखकर और Canvas से उनके पीछे कनेक्टर बनाकर Mac ऐप में शाखित ब्रैकेट आरेख बनाएँ। दो-अक्ष वाला स्क्रॉल दृश्य आरेख को तब भी पढ़ने योग्य रखता है जब विंडो उसकी सामग्री से छोटी हो।
यह ट्यूटोरियल Node 1 जैसे तटस्थ लेबल वाले सात नोड का एक कृत्रिम आरेख बनाता है। शाखाएँ दाईं ओर स्थित मूल नोड से बाईं ओर फैलती हैं। ठोस कार्य यह है कि आकार बदल सकने वाली macOS विंडो में जुड़े हुए, समान अंतर वाले कार्ड दिखाए जाएँ, जिनकी स्थिति और रेखांकन एक ही ज्यामिति से संचालित हों।
ऐसा लेआउट ढाँचा चुनें जो कार्ड और रेखाओं को संरेखित रखे
इस कार्यान्वयन के लिए macOS 13 या उसके बाद का डिप्लॉयमेंट टार्गेट चुनें। Apple के दस्तावेज़ों के अनुसार Layout macOS 13 से उपलब्ध है। इसके दो आवश्यक तरीके कंटेनर का आकार बताते हैं और उसके सबव्यू को रखते हैं। इसलिए यह गणना की गई स्थितियों पर कार्ड व्यवस्थित करने के लिए उपयुक्त है। Layout
नोड कार्ड सामान्य SwiftUI व्यू ही रखें। कनेक्टर स्ट्रोक के लिए केवल पृष्ठभूमि में Canvas का उपयोग करें। Apple बताता है कि कैनवस के भीतर बनाए गए अलग-अलग तत्वों को अपनी इंटरैक्टिविटी या पहुँच-योग्यता नहीं मिलती, इसमें कैनवस प्रतीकों के रूप में दिए गए व्यू भी शामिल हैं। कार्ड अलग रखने से बाद में सामान्य नियंत्रण जोड़ना आसान रहता है। Canvas
कार्यान्वयन के तीन हिस्से हैं: शाखाओं का वर्णन करने वाला ट्री, नोड केंद्र और किनारे तैयार करने वाली ज्यामिति गणना, और कार्डों को कनेक्टर रेखांकन के साथ जोड़ने वाला व्यू। दोनों रेंडरिंग परतों को एक ही ज्यामिति मान मिलता है। इसलिए ट्री बदलने पर कार्ड अपडेट होकर पीछे अलग से सँभाली गई कनेक्टर सूची पुरानी नहीं रह जाती।
आकार निर्धारण की नीति जानबूझकर स्थिर रखी गई है: विंडो बदलने पर कार्ड का आकार वही रहता है और आसपास का व्यूपोर्ट स्क्रॉल होता है। Apple के दस्तावेज़ों के अनुसार ScrollView क्षैतिज, ऊर्ध्वाधर या दोनों दिशाओं में स्क्रॉल कर सकता है; यह ज़ूम की सुविधा नहीं देता। ScrollView
पत्ती अंतर से शाखाओं की स्थिति निकालें
पत्तियों से शुरू करें—ऐसे नोड जिनकी कोई संतान नहीं है। हर पत्ती को क्रमशः एक ऊर्ध्वाधर स्लॉट दें। फिर हर मूल नोड को उसकी पहली और आखिरी संतान के बीच ठीक मध्य में रखें। क्षैतिज स्थिति गहराई पर निर्भर करती है: मूल नोड सबसे दाएँ कॉलम में रहता है और हर अतिरिक्त गहराई उसे एक कॉलम बाईं ओर ले जाती है।
इस उदाहरण में 132 × 44 पॉइंट के कार्ड, 64 पॉइंट का क्षैतिज अंतर और 28 पॉइंट का ऊर्ध्वाधर अंतर रखें। ये केवल उदाहरण के लिए दिए गए डिज़ाइन इनपुट हैं। तीन कॉलम और चार पत्तियों के साथ, बाहरी पैडिंग से पहले सामग्री का आकार 524 × 260 पॉइंट होगा।
| मात्रा | गणना | परिणाम |
|---|---|---|
| कॉलम चरण | कार्ड की चौड़ाई + क्षैतिज अंतर | 196 पॉइंट |
| पत्ती चरण | कार्ड की ऊँचाई + ऊर्ध्वाधर अंतर | 72 पॉइंट |
| सामग्री की चौड़ाई | 3 × 132 + 2 × 64 | 524 पॉइंट |
| सामग्री की ऊँचाई | 4 × 44 + 3 × 28 | 260 पॉइंट |
इससे चार पत्तियों के केंद्रों के ऊर्ध्वाधर निर्देशांक 22, 94, 166 और 238 मिलते हैं। उनके तुरंत ऊपर के मूल नोड 58 और 202 पर होंगे, जबकि सबसे ऊपर का मूल नोड 130 पर होगा। ये निर्देशांक जाँच के लिए उपयोगी हैं: मूल नोड अपने बाहरी संतान-केंद्रों के बीच में होना चाहिए और हर कार्ड रिपोर्ट किए गए सामग्री आयत के भीतर आना चाहिए।
नीचे दिया गया एल्गोरिदम असमान शाखा गहराइयों को भी स्वीकार करता है। कम गहराई वाली पत्ती अपने गहराई वाले कॉलम में रहती है; वह अपने आप सबसे बाएँ कॉलम में नहीं जाती। इस उदाहरण में यह नियम जानबूझकर चुना गया है। निर्देशांक सूत्र बदलने से पहले तय करें कि आपके आरेख के लिए गहराई के अनुसार या पत्ती के अनुसार संरेखण बेहतर है।
पूरा macOS उदाहरण बनाएँ
macOS SwiftUI ऐप टार्गेट और macOS 13 या उसके बाद का डिप्लॉयमेंट टार्गेट वाला Xcode प्रोजेक्ट इस्तेमाल करें। इसकी बनाई हुई ऐप एंट्री फ़ाइल को नीचे दिए गए तीन कोड ब्लॉक से, इसी क्रम में, एक Swift फ़ाइल में बदलें। ये मिलकर पूरा प्रोग्राम बनाते हैं। अगर पुरानी ऐप घोषणा किसी दूसरी फ़ाइल में है, तो उसे हटाएँ: Apple के अनुसार ऐप एंट्री पॉइंट ठीक एक होना चाहिए। App
ट्री और उसकी ज्यामिति तय करें
हर नोड में एक पूर्णांक पहचानकर्ता और संतानों का एक ऐरे होता है। ज्यामिति बिल्डर मूल नोड का केंद्र निकालने से पहले संतानों पर जाता है और नोड की स्थितियाँ तथा किनारे, दोनों इकट्ठा करता है। दोहराया गया पहचानकर्ता अस्पष्ट व्यू संग्रह बनाने के बजाय प्री-कंडीशन विफलता उत्पन्न करता है।
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)
}
}
मॉडल में हमेशा एक मूल नोड होता है, इसलिए संतान-विहीन ट्री भी एक कार्ड और गैर-शून्य सामग्री आकार देता है। इस प्रोग्राम में खाली आरेख के लिए अलग स्थिति नहीं है। यदि आपके ऐप में मूल नोड न होना संभव है, तो यह ज्यामिति बनाने से पहले उस स्थिति को स्पष्ट रूप से सँभालें।
कस्टम लेआउट मान से कार्ड रखें
हर कार्ड का केंद्र LayoutValueKey के ज़रिए दें। Apple के दस्तावेज़ों में बताए गए तरीके में layoutValue(key:value:) से मान सेट किया जाता है, फिर कुंजी का उपयोग करके सबव्यू प्रॉक्सी से उसे पढ़ा जाता है। इससे समानांतर ऐरे इंडेक्स पर निर्भर रहने के बजाय स्थिति कार्ड से जुड़ी रहती है। 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
)
}
}
}
लेआउट हर प्रपोज़ल के लिए वही सीमित आकार लौटाता है, क्योंकि इसकी सामग्री लचीली नहीं है। Apple स्पष्ट रूप से दिखाता है कि ऐसा लेआउट प्रपोज़ल को अनदेखा कर सकता है। लेआउट पास के दौरान SwiftUI अलग-अलग प्रपोज़ल के साथ आकार निर्धारण विधि को कई बार बुला सकता है, इसलिए यह विधि केवल ज्यामिति लौटाती है; यह मॉडल में कोई अपडेट नहीं करती। sizeThatFits(proposal:subviews:cache:)
प्लेसमेंट हर स्थानीय केंद्र में bounds.minX और bounds.minY जोड़ता है। दी गई सीमाएँ पैरेंट के निर्देशांक स्थान में होती हैं, इसलिए शून्य मूल मान लेने पर कंटेनर के खिसकने पर प्लेसमेंट बिगड़ जाएगा। Apple यह भी अपेक्षा करता है कि आकार निर्धारण और प्लेसमेंट की गणनाएँ अंतर तथा आयामों के बारे में एकमत हों। placeSubviews(in:proposal:subviews:cache:)
.center का उपयोग करने से दिया गया बिंदु हर कार्ड का मध्य दर्शाता है। सबव्यू प्लेसमेंट API के अनुसार हर प्रॉक्सी को प्लेसमेंट विधि के भीतर ठीक एक बार रखा जाता है। अगले कोड ब्लॉक में कार्ड के स्पष्ट फ़्रेम यह सुनिश्चित करते हैं कि दिखाए गए कार्डों के आयाम ज्यामिति के आयामों से मेल खाएँ। place(at:anchor:proposal:)
कनेक्टर बनाएँ और विंडो दिखाएँ
हर कनेक्टर संतान कार्ड के दाएँ किनारे से शुरू होकर मूल कार्ड के बाएँ किनारे पर समाप्त होता है। बीच के दो बिंदु अंतराल के मध्य से गुज़रता हुआ समकोण मार्ग बनाते हैं। Path.addLines(_:) बिंदुओं के ऐरे से जुड़ी हुई सीधी रेखाएँ बनाता है। 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 को पहचान योग्य नोड मान मिलते हैं, जो Apple की इस अपेक्षा के अनुरूप है कि तत्व या तो पहचान योग्य हों या स्पष्ट पहचानकर्ता दिया जाए। यदि आगे ट्री संपादन योग्य बनाते हैं, तो पहचानकर्ता तार्किक नोड से जुड़े रखें। ForEach
कैनवस सीधे लेआउट की पृष्ठभूमि से जुड़ा है, इसलिए उसके स्थानीय निर्देशांक कार्डों के लिए इस्तेमाल की गई ज्यामिति से मेल खाते हैं। आरेख तैयार होने के बाद बाहरी पैडिंग लगाई जाती है, जिससे दोनों परतें साथ खिसकती हैं। भाई-बहन कनेक्टरों के साझा हिस्से एक-दूसरे पर आकर ब्रैकेट जैसा जोड़ बनाते हैं।
WindowGroup विंडो का व्यू पदानुक्रम और macOS की मानक विंडो प्रबंधन सुविधाएँ देता है। इस नमूने में ऐप संरचना के भीतर अपरिवर्तनीय कृत्रिम ज्यामिति रखी गई है; व्यू की स्थिति बदलने के लिए इसमें टाइमर या लाइफ़साइकल कॉलबैक नहीं लगाए गए हैं। WindowGroup
आरेख जाँचें और आम समस्याओं का पता लगाएँ
इस नमूने का अंकगणित एक छोटे नियतात्मक Python हिसाब से जाँचा गया: सात केंद्र, छह किनारे, 524 × 260 पॉइंट का सामग्री आयत और उस आयत के भीतर हर कार्ड। SwiftUI प्रोग्राम की जाँच की गई, लेकिन शोध परिवेश में Swift कंपाइलर उपलब्ध न होने के कारण इसे कंपाइल नहीं किया जा सका।
Xcode में ऐप बनाते समय इन दिखाई देने वाले नतीजों की जाँच करें:
- सबसे बाएँ कॉलम में चार कार्ड, बीच में दो और दाईं ओर एक कार्ड दिखे।
- हर कनेक्टर संबंधित कार्ड के किनारे के क्षैतिज मध्यबिंदु को छुए।
- हर मूल नोड अपने बाहरी बच्चों के बीच ऊर्ध्वाधर रूप से स्थित हो।
- विंडो का आकार बदलने पर कार्ड के आयाम स्थिर रहें और ज़रूरत पड़ने पर स्क्रॉलिंग उपलब्ध हो।
- नमूने के मूल नोड को
BranchNode(1)से बदलने पर बिना कनेक्टर वाला एक कार्ड दिखे।
अगर कार्ड और रेखाएँ खिसकी हुई हों, तो पहले मॉडिफ़ायर की जगह देखें। केवल कार्डों पर पैडिंग या ऑफ़सेट लगाने से निर्देशांकों में दूसरा समायोजन जुड़ जाता है, जो कनेक्टर की ज्यामिति पर लागू नहीं होता। दिखाए अनुसार आरेख-स्तरीय अंतर BranchDiagram के बाहर रखें।
अगर कोई कार्ड अनपेक्षित रूप से लेआउट के मूल बिंदु पर दिखे, तो जाँचें कि उसके अंतिम व्यू में DiagramCenterKey का मान है। कुंजी का डिफ़ॉल्ट .zero है; मान न देने पर कार्ड का केंद्र मूल बिंदु पर रखा जाता है। अगर प्रोग्राम पहचानकर्ता वाली प्री-कंडीशन पर रुकता है, तो रेंडर करने से पहले हर नोड को अलग पहचानकर्ता दें।
इस उदाहरण के छोटे कृत्रिम लेबल के लिए कार्ड के स्थिर आयाम उपयुक्त हैं। लंबे टेक्स्ट या बड़े फ़ॉन्ट के लिए मापन चरण जोड़ें: सबव्यू प्रॉक्सी से उनके आकार पूछें, उन मापों के आधार पर कॉलम की चौड़ाई और सबट्री का अंतर निकालें, और कनेक्टर के सिरों के लिए भी उन्हीं नतीजों का उपयोग करें। केवल आकार का प्रपोज़ल देने से सबव्यू को वह आकार स्वीकार करने के लिए बाध्य नहीं किया जा सकता। place(at:anchor:proposal:)
यह एल्गोरिदम ट्री का वर्णन करता है: हर दिखाया गया बच्चा एक ही मूल नोड का होता है। यह मनमाने क्रॉस-लिंक का मार्ग नहीं बनाता और न ही एक ही साझा नोड को कई मूल नोड के नीचे दिखाता है। ऐसी ज़रूरतों के लिए अलग ज्यामिति मॉडल और स्पष्ट मार्ग-निर्धारण नीति चाहिए। इसी तरह, ऐनिमेशन के लिए कनेक्टर ज्यामिति को कार्डों की स्थिति के साथ चलना होगा; यह उदाहरण स्थिर प्लेसमेंट देता है।
सामग्री का आकार, कार्ड केंद्र और किनारों के सिरों की ज़िम्मेदारी एक ही ज्यामिति गणना को दें। इससे अंतर समायोजित करने, निर्देशांक देखने और आरेख बढ़ाने के लिए एक ठोस जगह मिलती है, साथ ही SwiftUI लेआउट और रेखांकन में तालमेल बना रहता है।
स्रोत
- 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