मापे गए आकार से वापस उसी फ़्रेम तक जाने वाला रास्ता हटाकर SwiftUI ज्यामिति लूप रोकें, जिसने वह आकार बनाया था। साझा चौड़ाई वाले लेबलों वाली Mac विंडो के लिए, मापों को स्थिति में सहेजने के बजाय Layout कंटेनर के भीतर उनके आकार की गणना करें।
यह वॉकथ्रू आपके अपने ऐप की विंडो के भीतर एक छोटा लेबल पैनल बनाता है। लेबलों में स्थानीय रूप से बनाया गया टेक्स्ट है, उनकी चौड़ाई समान है और विंडो संकरी होने पर वे अगली पंक्ति में चले जाते हैं। पूरा उदाहरण macOS 13 या उसके बाद के संस्करण को लक्ष्य बनाता है; बाद के अनुभाग बताते हैं कि जब आपके इंटरफ़ेस में AppKit टेक्स्ट व्यू शामिल हों, तब भी आकार निर्धारण के इसी अनुशासन को कैसे लागू करें।
ज्यामिति का माप लेआउट लूप कैसे बन जाता है?
SwiftUI लेआउट में प्रस्ताव और जवाब शामिल होते हैं: पैरेंट आकार का प्रस्ताव देता है और उसके चिल्ड्रन अपने चुने हुए आकार बताते हैं। कोई कस्टम कंटेनर ProposedViewSize के ज़रिए इस बातचीत में हिस्सा लेता है। इसलिए किसी व्यू को बार-बार मापना सामान्य है; केवल बार-बार मापे जाने से यह साबित नहीं होता कि कुछ गड़बड़ है। Apple का ProposedViewSize documentation देखें।
समस्या तब पैदा होती है जब कोई माप उस लेआउट के इनपुट को बदल देता है जिसे मापा जा रहा है। उदाहरण के लिए, कोई लेबल पैडिंग सहित अपनी चौड़ाई बताता है, कोई ऐक्शन उस चौड़ाई को सहेजता है और आसपास का फ़्रेम सहेजे गए मान में और पैडिंग जोड़कर उसका उपयोग करता है। इससे बना फ़्रेम अगला माप बदल देता है। Apple ने इस geometry-reader-to-frame फ़ीडबैक लूप का स्पष्ट वर्णन किया है और इस तरह की व्यवस्था के लिए लेआउट सिस्टम इस्तेमाल करने की सलाह दी है। Compose custom layouts with SwiftUI देखें।
एक आसान गणना दिखाती है कि समानता जाँच से मदद क्यों नहीं मिल सकती। मान लीजिए कि एक उदाहरणात्मक कार्यान्वयन पिछले फ़्रेम की चौड़ाई मापता है, उसमें 16 पॉइंट पैडिंग जोड़ता है और फिर परिणाम को अगली फ़्रेम चौड़ाई के रूप में तय करता है। 120 पॉइंट से शुरू करने पर चौड़ाइयाँ 136, 152 और 168 पॉइंट होंगी। हर परिणाम पिछले परिणाम से अलग है, इसलिए अपरिवर्तित मान असाइन होने से रोकने वाली जाँच बढ़ोतरी को कभी नहीं रोकती। ये संख्याएँ निर्भरता समझाने के लिए हैं; ये किसी चल रहे ऐप के माप नहीं हैं।
कोई समाधान चुनने से पहले अपने कोड में निर्भरता का पता लगाएँ:
- ज्यामिति रीडर, प्रेफरेंस हैंडलर या ज्यामिति-बदलाव ऐक्शन खोजें।
- माप प्राप्त करने वाले हर स्थिति मान की पहचान करें।
- उन मानों पर निर्भर फ़्रेम, पैडिंग, फ़ॉन्ट विकल्प या सशर्त व्यू खोजें।
- जाँचें कि क्या ये विकल्प मापे जा रहे व्यू या उसके पूर्वजों को प्रभावित करते हैं।
- अस्थायी रूप से मापे गए मान को किसी स्थिर मान से बदलें। अगर बार-बार होने वाले बदलाव रुक जाएँ, तो पहले उसी निर्भरता पथ की जाँच करें।
माप को आगे पहुँचाने का तरीका बदलने से चक्र ज़रूरी नहीं कि हट जाए। कोई प्रेफरेंस वही आकार वापस उसी पूर्वज तक पहुँचा सकता है। स्थिति का असाइनमेंट टालने से फ़ीडबैक अगले अपडेट में जा सकता है। अहम सवाल यह है कि क्या अगला माप अब भी पिछले परिणाम पर निर्भर करता है।
किस आकार-निर्धारण तरीके का इस्तेमाल करना चाहिए?
जिस नतीजे की आपको ज़रूरत है, उसके अनुसार तरीका चुनें। पहले से मौजूद लेआउट कंटेनरों से शुरुआत करें; Apple के अनुसार अधिक नियंत्रण की ज़रूरत वाले व्यवहार के लिए कस्टम Layout कार्यान्वयन अपनाएँ। Layout documentation देखें।
| ज़रूरी नतीजा | समर्थित तरीका | मुख्य शर्त |
|---|---|---|
| सामान्य लेबलों को लंबवत व्यवस्थित करना | पहले से मौजूद स्टैक | चिल्ड्रन को सामान्य आकार निर्धारण में हिस्सा लेने दें। |
| चौड़े या कॉम्पैक्ट रूप में से एक चुनना | ViewThatFits |
विकल्पों को प्राथमिकता के क्रम में दें। |
| कई लेबलों को सामग्री के आधार पर समान चौड़ाई देना | कस्टम Layout |
लेआउट मेथड के भीतर चिल्ड्रन को मापें और रखें। |
| किसी मौजूदा व्यू की ज्यामिति पर प्रतिक्रिया देना | onGeometryChange |
ऐक्शन के लिए केवल ज़रूरी मान पर नज़र रखें। |
| एम्बेड किए गए AppKit व्यू का आकार तय करना | NSViewRepresentable.sizeThatFits |
प्रस्ताव के लिए पसंदीदा आकार लौटाएँ। |
ViewThatFits उन चिल्ड्रन में से पहला चुनता है जिसका आदर्श आकार सीमित अक्षों में फिट होता है। यह तब उपयोगी है जब आपकी वास्तविक ज़रूरत अलग-अलग रूपों में से चुनना हो, जैसे क्षैतिज व्यवस्था और लंबवत विकल्प। इससे अलग मापी गई-चौड़ाई वाला स्थिति चर हट सकता है। Apple का ViewThatFits documentation देखें।
नीचे दिए गए समान-चौड़ाई वाले लेबल पैनल का आकार निर्धारण नियम स्पष्ट है: सबसे चौड़े आदर्श लेबल की चौड़ाई लें और उसे प्रस्तावित सीमित चौड़ाई तक सीमित रखें। फिर चुनी हुई चौड़ाई पर हर लेबल की ऊँचाई मापें। इससे संकरी विंडो में टेक्स्ट अगली पंक्ति में जाने के कारण पैनल की ऊँचाई बढ़ सकती है, और ज्यामिति मापों को ऐप की स्थिति से गुज़ारने की ज़रूरत नहीं पड़ती।
ज्यामिति की स्थिति के बिना समान-चौड़ाई वाला लेबल पैनल बनाएँ
Xcode में SwiftUI का उपयोग करके macOS ऐप प्रोजेक्ट बनाएँ, उसका डिप्लॉयमेंट टार्गेट macOS 13 या उसके बाद के संस्करण पर सेट करें और ऐप तथा कंटेंट व्यू की बनाई गई घोषणाओं को इस पूरी सिंगल-फ़ाइल मिसाल से बदलें। टार्गेट में केवल एक @main ऐप घोषणा रखें। इस प्रोग्राम को दस्तावेज़ीकृत API के संदर्भ में जाँचा गया है; उपलब्ध वातावरण 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()
}
}
}
दो आवश्यक लेआउट मेथड काम बाँटते हैं: sizeThatFits कंटेनर का आकार बताता है, जबकि placeSubviews उसके चिल्ड्रन को उनकी जगह पर रखता है। चाइल्ड प्रॉक्सी, अंतर्निहित व्यू तक सीधे पहुँचे बिना मापने और रखने की सुविधा देती हैं। दोनों मेथड दस्तावेज़ीकृत ज़िम्मेदारियों का पालन करते हैं। Apple का Layout documentation देखें।
कार्यान्वयन के अहम हिस्से चौड़ाई का प्रस्ताव और उसका दोबारा इस्तेमाल हैं। हर चाइल्ड को निश्चित चौड़ाई और अनिर्दिष्ट ऊँचाई के साथ मापा जाता है, ताकि टेक्स्ट अपनी ज़रूरी ऊँचाई तय कर सके। चिल्ड्रन को रखने के लिए कंटेनर की वास्तविक सीमाओं की चौड़ाई और ऊँचाई तय करने की वही नीति इस्तेमाल होती है। शुरुआत bounds.minX और bounds.minY से करने पर यह मानकर नहीं चला जाता कि प्लेसमेंट निर्देशांक के मूल बिंदु से शुरू होता है।
कंटेनर खाली संग्रह के लिए शून्य लौटाता है। अनिर्दिष्ट चौड़ाई होने पर यह आदर्श चौड़ाई चुनता है; सीमित चौड़ाई होने पर आदर्श चौड़ाई को उसी तक सीमित रखता है। इन लेबलों के लिए अनंत चौड़ाई का प्रस्ताव भी सीमित आदर्श चौड़ाई देता है। सामग्री के आकार पर आधारित इस पैनल के लिए ये सोच-समझकर चुने गए नियम हैं। Apple बताता है कि लेआउट के एक चरण के दौरान पैरेंट, लेआउट को शून्य, अनंत, अनिर्दिष्ट और दूसरे प्रस्तावों के साथ जाँच सकते हैं। sizeThatFits(proposal:subviews:cache:) देखें।
इस उदाहरण में मापे गए आकार की कोई स्थिति नहीं, ज्यामिति कॉलबैक नहीं और रेंडर की गई चौड़ाई को वापस फ़्रेम में असाइन नहीं किया जाता। इसका लंबवत स्क्रॉल व्यू अगली पंक्तियों में गए लंबे कंटेंट को जगह देता है। यह लेआउट दिए गए लेबलों जैसे चिल्ड्रन मानकर चलता है, जो सीमित चौड़ाई के अनुसार ठीक से आकार बदल सकते हैं। उपलब्ध जगह से बड़े किसी निश्चित-चौड़ाई वाले चाइल्ड के लिए अलग प्रस्तुति विकल्प चाहिए, जैसे क्षैतिज स्क्रॉलिंग या कोई वैकल्पिक व्यू।
ज्यामिति पर नज़र रखना कब सही रहता है?
जब माप किसी सीमित प्रतिक्रिया को चलाता है, तब भी ज्यामिति पर नज़र रखना उपयोगी है। Apple का onGeometryChange API ट्रांसफ़ॉर्म और ऐक्शन को अलग करता है: ट्रांसफ़ॉर्म इक्वेटेबल मान निकालता है और उस रूपांतरित मान के बदलने पर ऐक्शन चलता है। Apple व्यापक ऐप अपडेट से बचने की सलाह देता है, क्योंकि ज्यामिति अक्सर बदल सकती है। onGeometryChange(for:of:action:) देखें।
उदाहरण के लिए, कोई ओवरले दिखा सकता है कि स्थिर कंटेनिंग क्षेत्र चुनी हुई चौड़ाई से बड़ा है या नहीं। उस कंटेनिंग क्षेत्र को देखें और ट्रांसफ़ॉर्म से बूलियन लौटाएँ। ओवरले को क्षेत्र के आकार में योगदान न देने दें। यह डिज़ाइन दस्तावेज़ीकृत ट्रांसफ़ॉर्म-और-ऐक्शन मॉडल का पालन करता है और गोल आकार-निर्धारण निर्भरता हटाता है।
किसी रेक्टैंगल को बूलियन में बदलने से नोटिफ़िकेशन सीमित होते हैं, पर इससे अपने आप स्थिरता साबित नहीं होती। अगर कॉम्पैक्ट रूप में जाने से मापा गया क्षेत्र इतना बदलता है कि वापस पिछले रूप में जाना पड़े, तो बूलियन लगातार बदल सकता है। ऐसे क्षेत्र को मापें जिसकी चौड़ाई आसपास के विंडो लेआउट से आती हो, या प्रस्तुति का चुनाव लेआउट के भीतर करने के लिए ViewThatFits इस्तेमाल करें।
उपलब्धता ओवरलोड पर भी निर्भर करती है। एक नए मान वाला ऐक्शन macOS 13 या उसके बाद के संस्करण पर उपलब्ध है, बशर्ते उसे ऐसे SDK से बनाया गया हो जिसमें यह शामिल हो; पुराने और नए मान वाला ओवरलोड macOS 15 या उसके बाद के संस्करण के लिए है। पूरा उदाहरण केवल Layout इस्तेमाल करके इस अंतर से बचता है। किसी पुराने डिप्लॉयमेंट टार्गेट में कॉलबैक सिग्नेचर कॉपी करने से पहले Apple का geometry-change documentation और old-and-new-value overload देखें।
AppKit और TextKit के माप SwiftUI लेआउट में कैसे शामिल होने चाहिए?
जब कंटेंट AppKit व्यू हो, तो रिप्रेज़ेंटेबल की आकार-निर्धारण सीमा का इस्तेमाल करें। NSViewRepresentable.sizeThatFits(_:nsView:context:) macOS 13 या उसके बाद के संस्करण पर उपलब्ध है, प्रस्ताव के लिए पसंदीदा आकार लौटाता है और एक ही चरण में अलग-अलग प्रस्तावों के साथ कई बार बुलाया जा सकता है। nil लौटाने से डिफ़ॉल्ट आकार-निर्धारण एल्गोरिदम इस्तेमाल करने का अनुरोध होता है। इसे आकार निर्धारण की पूछताछ मानें, मापे गए आयामों को SwiftUI स्थिति में डालने की जगह नहीं। the representable sizing method देखें।
SwiftUI, दर्शाए गए AppKit व्यू के frame और bounds को नियंत्रित करता है। Apple साफ़ तौर पर इन लेआउट गुणों को खुद सेट करने के खिलाफ़ चेतावनी देता है, क्योंकि इससे SwiftUI के साथ टकराव होता है और व्यवहार अनिर्धारित हो जाता है। रिप्रेज़ेंटेबल के अपडेट मेथड में कंटेंट कॉन्फ़िगर करें और समर्थित आकार-निर्धारण तरीके से आकार बताएँ। NSViewRepresentable देखें।
टेक्स्ट के लिए पहले तय करें कि व्यू कौन सा लेआउट इंजन इस्तेमाल करता है। TextKit 2 में NSTextLayoutManager.usageBoundsForTextContainer macOS 12 या उसके बाद के संस्करण पर उपलब्ध है और टेक्स्ट कंटेनर के उपयोग-क्षेत्र का माप देता है। Apple इसे KVO के ज़रिए देखे जा सकने वाले गुण के रूप में दस्तावेज़ीकृत करता है। यह निगरानी आकार बदलने का अनुरोध करने में मदद कर सकती है, लेकिन इससे चौड़ाई और ऊँचाई की निर्भरताओं को एकसार रखने की ज़रूरत खत्म नहीं होती। usageBoundsForTextContainer देखें।
मौजूदा TextKit 1 कार्यान्वयन के लिए, NSLayoutManager.usedRect(for:) पहले से लेआउट किए गए ग्लिफ़ों का घेरा हुआ क्षेत्र बताता है; यह ग्लिफ़ नहीं बनाता और न ही लेआउट करता है। अगर माप के लिए लेआउट पूरा होना ज़रूरी है, तो उपयोग किए गए रेक्टैंगल को पढ़ने से पहले संबंधित कंटेनर के लिए ensureLayout(for:) बुलाएँ। दोनों API macOS 10.7 से उपलब्ध हैं। usedRect(for:) और ensureLayout(for:) देखें।
माप लेने के लिए TextKit 2 टेक्स्ट व्यू के पुराने layoutManager तक न पहुँचें। Apple बताता है कि पुराने लेआउट मैनेजर तक पहुँचने से टेक्स्ट व्यू TextKit 1 संगतता मोड में जा सकता है। पहले textLayoutManager जाँचें और पुराने माप वाले कोड को सही फ़ॉलबैक शाखा में रखें। What’s new in TextKit and text views देखें।
अगली पंक्ति में जाने वाले टेक्स्ट व्यू के लिए प्रस्तावित चौड़ाई को टेक्स्ट लेआउट के इनपुट के रूप में रखें और उसी कॉन्फ़िगरेशन से पसंदीदा ऊँचाई निकालें। टेक्स्ट व्यू के इनसेट और कंटेनर पैडिंग को स्पष्ट रूप से गिनें; कंटेनर निर्देशांक का कोई रेक्टैंगल अपने आप बाहरी व्यू का आकार नहीं होता। AppKit UI का काम उसी ऐक्टर आइसोलेशन में रखें। Swift की Concurrency chapter में मेन ऐक्टर के निष्पादन की व्याख्या है; काम को टास्क में भेजने से आकार-निर्धारण सूत्र की गोल निर्भरता नहीं बदलती।
कैसे जाँचें कि लेआउट अब स्थिर हो जाता है?
Xcode में उदाहरण चलाएँ और विंडो को दोनों दिशाओं में धीरे-धीरे आकार दें। अपेक्षित व्यवहार है कि लेबलों की चौड़ाई समान रहे, संकरी चौड़ाई पर वे अगली पंक्ति में जाएँ और कंटेंट दिखाई देने वाली ऊँचाई से अधिक होने पर लंबवत स्क्रॉल हो। फिर मापी गई स्थिति की निर्भरता हटाने के बाद अपनी वास्तविक व्यू संरचना के साथ यही जाँच दोहराएँ।
अपेक्षित लेआउट काम और लगातार चल रहे फ़ीडबैक में अंतर समझने के लिए ये जाँचें करें:
- आकार बदलने के बाद विंडो को स्थिर छोड़ें। देखें कि क्या कंटेंट बदले बिना भी माप और स्थिति असाइनमेंट लगातार बदलते रहते हैं।
- कोई लंबा कृत्रिम लेबल और बड़ा फ़ॉन्ट आज़माएँ। पुष्टि करें कि टेक्स्ट कटे बिना पैनल लंबवत बढ़ सकता है।
- खाली लेबल संग्रह आज़माएँ। आसपास की पैडिंग लागू होने से पहले लेआउट को शून्य लौटाना चाहिए।
- माप और प्लेसमेंट, दोनों के प्रस्ताव देखें। अगली पंक्ति में जाने वाले चाइल्ड को चौड़ाई की एकसार धारणाएँ मिलनी चाहिए।
- अगर बाद में कैशिंग जोड़ें, तो उसकी वैधता के नियमों में आकार-निर्धारण के इनपुट शामिल करें; किसी दूसरी चौड़ाई के लिए निकाली गई ऊँचाइयों का दोबारा इस्तेमाल न करें।
एक ही चरण के दौरान कई आकार-निर्धारण कॉल दस्तावेज़ीकृत व्यवहार हैं। व्यावहारिक लक्ष्य अपरिवर्तित कंटेंट और सीमाओं के लिए स्थिर नतीजा है। जब माप व्यवस्था तय करते हों, तो उन्हें लेआउट बातचीत के भीतर रखें और ज्यामिति की निगरानी उन प्रतिक्रियाओं के लिए रखें, जो देखे गए आयामों को फिर से न बनाएँ।
स्रोत
- Compose custom layouts with SwiftUI — Apple
- ProposedViewSize — Apple
- Layout — Apple
- ViewThatFits — Apple
- sizeThatFits(proposal:subviews:cache:) — Apple
- onGeometryChange(for:of:action:) — Apple
- onGeometryChange(for:of:action:) — old and new values — Apple
- sizeThatFits(_:nsView:context:) — Apple
- NSViewRepresentable — Apple
- usageBoundsForTextContainer — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- What’s new in TextKit and text views — Apple
- Concurrency — The Swift Programming Language — Swift.org