SwiftUI पंक्ति का बार-बार आरंभीकरण यह साबित नहीं करता कि पंक्ति गायब हो गई या उसने अपनी स्थिति खो दी। स्क्रॉल होने वाले Mac इंटरफ़ेस की जाँच के लिए निर्माण, बनाए रखी गई स्थिति, दिखाई देने वाले कॉलबैक और व्यूपोर्ट में दृश्यता को अलग-अलग ट्रैक करें।
यह मार्गदर्शिका ऐसे डेवलपर के लिए है जो अपने macOS ऐप में पंक्तियों के बार-बार लॉग होने की जाँच कर रहा है। इसका ठोस उद्देश्य यह पता लगाना है कि कोई अपडेट केवल एक और व्यू वैल्यू बनाता है, पंक्ति की स्थिति रीसेट करता है, या स्क्रॉल व्यूपोर्ट में मौजूद पंक्तियों को बदलता है। उदाहरण में कृत्रिम लेबल और सामान्य बटन इस्तेमाल किए गए हैं।
जीवनचक्र का हर संकेत वास्तव में क्या बताता है?
SwiftUI की पंक्ति आमतौर पर एक संरचना का इस्तेमाल करती है, जो Swift का वैल्यू टाइप है। एक और संरचना वैल्यू बनाना, स्थायी रेफ़रेंस ऑब्जेक्ट बनाने से अलग है। Swift के दस्तावेज़ Swift में वैल्यू और रेफ़रेंस टाइप में यह अंतर समझाते हैं।
SwiftUI का अपना पहचान मॉडल भी है। Apple बताता है कि अस्थायी व्यू वैल्यू अपडेट के दौरान एक ही पहचान का प्रतिनिधित्व कर सकती हैं और व्यू का जीवनकाल उसी पहचान से जुड़ा होता है। इसलिए, इनिशियलाइज़र लॉग यह साबित नहीं कर सकता कि पिछली पंक्ति का जीवनकाल समाप्त हो गया। देखें Demystify SwiftUI।
अलग-अलग सवालों के जवाब पाने के लिए इन संकेतों का उपयोग करें:
| संकेत | यह किस सवाल का जवाब देता है | व्याख्या की सीमा |
|---|---|---|
| पंक्ति इनिशियलाइज़र | क्या पंक्ति की एक और वैल्यू बनाई गई? | इससे दिखाई देना या स्थिति का बदलना साबित नहीं होता। |
body का मूल्यांकन |
क्या इस व्यू विवरण का मूल्यांकन हुआ? | इससे प्रदर्शित फ़्रेमों की गिनती नहीं होती। |
| बनाए रखी गई पंक्ति की स्थिति | क्या यह निदान वैल्यू अपडेट के दौरान बनी रही? | यह उस स्थिति की निरंतरता बताती है, व्यूपोर्ट में मौजूदगी नहीं। |
onAppear और onDisappear |
क्या SwiftUI ने दिखाई देने के बदलाव की सूचना दी? | समय व्यू के प्रकार पर निर्भर करता है। |
onScrollVisibilityChange |
क्या पंक्ति ने चुनी गई व्यूपोर्ट सीमा पार की? | दस्तावेज़ में दी गई स्क्रॉल दृश्यता की शर्त मापता है। |
Apple के दस्तावेज़ों के अनुसार, onAppear पहले रेंडर किए गए फ़्रेम से पहले पूरा होता है; सटीक समय व्यू के प्रकार पर निर्भर करता है। onDisappear गायब होने के बाद चलता है, और इसका समय भी व्यू पर निर्भर करता है। इनमें से कोई भी विधि स्थिति के डीलोकेशन का दस्तावेज़ीकरण नहीं करती। इन कॉलबैक घटनाओं को स्टोरेज के जीवनकाल से जुड़े निष्कर्षों से अलग रखें। देखें onAppear(perform:) और onDisappear(perform:)।
व्यूपोर्ट से जुड़े सवाल के लिए स्पष्ट सीमा चुनें। macOS 15 और उसके बाद के संस्करणों में, onScrollVisibilityChange(threshold:_:) बताता है कि व्यू दी गई दृश्यता सीमा पार करता है या नहीं; डिफ़ॉल्ट 0.5 है। Apple यह भी बताता है कि व्यू दिखाई देने पर, यदि सीमा पहले से पूरी हो चुकी हो, तो शुरुआती कॉलबैक मिलता है। देखें onScrollVisibilityChange(threshold:_:)।
इस लेख में “व्यूपोर्ट में दृश्य” का अर्थ है कि स्क्रॉल की वह सीमा पूरी हो। इसका मतलब यह नहीं कि किसी व्यक्ति ने पंक्ति को देखा।
ऐसा छोटा प्रयोग बनाएँ जो संकेतों को अलग रखे
ऐसा macOS SwiftUI ऐप बनाएँ जिसके SDK में macOS 15 API शामिल हों और डिप्लॉयमेंट टार्गेट macOS 15 या उसके बाद का रखें। जनरेट किए गए ऐप और कंटेंट-व्यू घोषणाओं को नीचे दिए गए पूरे प्रोग्राम से बदलें। केवल एक @main एंट्री पॉइंट रखें।
उदाहरण में ScrollView और LazyVStack का उपयोग किया गया है ताकि व्यूपोर्ट का व्यवहार स्पष्ट रहे। Apple, LazyVStack को ऐसा बताता है जो ज़रूरत पड़ने पर आइटम बनाता है। यह प्रयोग यह नहीं मानता कि List का लोडिंग या बनाए रखने का व्यवहार भी वैसा ही है। देखें LazyVStack।
import SwiftUI
import Foundation
@main
struct RowLifetimeLabApp: App {
var body: some Scene {
WindowGroup("Row Lifetime Lab") {
ProbeView()
}
}
}
struct ProbeView: View {
@State private var revision = 0
@State private var generation = 0
var body: some View {
VStack(spacing: 12) {
HStack {
Button("Update rows") {
revision += 1
}
Button("Reset identity") {
generation += 1
}
Text("Revision \(revision)")
}
ScrollView {
LazyVStack(spacing: 8) {
ForEach(1...60, id: \.self) { rowID in
ProbeRow(rowID: rowID, revision: revision)
}
}
.padding()
}
.id(generation)
}
.padding()
.frame(minWidth: 560, minHeight: 360)
}
}
struct ProbeRow: View {
let rowID: Int
let revision: Int
private let constructionID: UUID
@State private var appearanceToken: UUID? = nil
@State private var count = 0
init(rowID: Int, revision: Int) {
self.rowID = rowID
self.revision = revision
self.constructionID = UUID()
print("init row=\(rowID) value=\(constructionID)")
}
var body: some View {
let _ = print("body row=\(rowID) value=\(constructionID)")
HStack {
VStack(alignment: .leading) {
Text("Sample row \(rowID)")
Text("Revision \(revision)")
Text(appearanceToken?.uuidString ?? "Awaiting appearance")
.font(.caption.monospaced())
}
Spacer()
Button("Count \(count)") {
count += 1
}
}
.padding(12)
.frame(height: 80)
.background(Color.gray.opacity(0.12))
.onAppear {
if appearanceToken == nil {
appearanceToken = UUID()
}
print("appear row=\(rowID) token=\(tokenDescription)")
}
.onDisappear {
print("disappear row=\(rowID) token=\(tokenDescription)")
}
.onScrollVisibilityChange(threshold: 0.5) { visible in
print("viewport row=\(rowID) visible=\(visible)")
}
}
private var tokenDescription: String {
appearanceToken?.uuidString ?? "unassigned"
}
}
दोनों UUID अलग-अलग काम करते हैं। constructionID हर इनिशियलाइज़र कॉल को लेबल करता है। appearanceToken खाली से शुरू होता है और पंक्ति की बनाए रखी गई स्थिति के लिए पहली देखी गई उपस्थिति के दौरान असाइन किया जाता है। यह स्थिति के आवंटन का समय दर्ज नहीं करता। गिनती आपको एक और दृश्य वैल्यू देती है जिसकी निरंतरता आप जाँच सकते हैं।
Apple के दस्तावेज़ों के अनुसार, SwiftUI State स्टोरेज को प्रबंधित करता है और उसकी वैल्यू बदलने पर निर्भर इंटरफ़ेस कंटेंट अपडेट करता है। इसलिए इस निदान में टोकन और गिनती को निजी स्थिति में रखा गया है, जबकि निर्माण की जानकारी सामान्य स्टोर की गई प्रॉपर्टी बनी रहती है। देखें State।
प्रोग्राम को दस्तावेज़ीकृत API के आधार पर जाँचा गया, लेकिन यहाँ कंपाइल या रन नहीं किया गया क्योंकि Swift टूलचेन उपलब्ध नहीं था। नीचे दिए गए लॉग उदाहरण बताते हैं कि अपने रन की व्याख्या कैसे करें; ये कैप्चर किए गए परिणाम नहीं हैं।
तीन नियंत्रित जाँच चलाएँ और परिणामों की व्याख्या करें
1. पहचान बदले बिना पंक्ति के इनपुट अपडेट करें
ऐप को Xcode में चलाएँ और डिबग कंसोल खुला रखें। किसी दिखाई देने वाली पंक्ति का Count बटन दो बार क्लिक करें, उसका टोकन नोट करें, फिर Update rows क्लिक करें। पंक्तियों को दिया गया रिविज़न बदलता है, जबकि निश्चित पूर्णांक पहचानकर्ता वही रहते हैं।
अगर इनिशियलाइज़र संदेश फिर दिखें, लेकिन चुनी हुई पंक्ति अपना टोकन और गिनती बनाए रखे, तो आपके पास इस बात का प्रमाण है कि वैल्यू का निर्माण दोहराया गया, जबकि निदान की स्थिति बनी रही। इन इनिशियलाइज़र संदेशों को अलग-अलग ऑनस्क्रीन जीवनकाल न मानें। प्रदर्शित रिविज़न पुष्टि करता है कि पंक्ति को नया इनपुट मिला।
उपस्थिति टोकन स्थिति के ज़रिए असाइन किया जाता है, जिससे एक और अपडेट भी हो सकता है। निदान के लिए यह व्यवहार जानबूझकर रखा गया है। इनिशियलाइज़र या body संदेशों की किसी निश्चित संख्या की अपेक्षा न करें और जाँच पूरी होने पर ये प्रोब हटा दें।
2. पंक्ति को व्यूपोर्ट से बाहर स्क्रॉल करके वापस लाएँ
धीरे-धीरे स्क्रॉल करें, जब तक चुनी हुई पंक्ति 50% सीमा पार न कर ले। viewport संदेशों की तुलना दिखाई देने के संदेशों से करें, फिर पंक्ति पर वापस जाएँ। उसके टोकन और गिनती फिर से देखें।
वापस आने पर टोकन बना रहे, तो यह उस आवागमन के दौरान इस निदान स्थिति की निरंतरता दिखाता है। अलग टोकन का मतलब है कि पिछला टोकन बनाए नहीं रखा गया। वास्तविक ऐप में रीसेट की वजह समझने के लिए, केवल स्क्रॉल को कारण मानने के बजाय पहचान में बदलाव, शर्त के आधार पर हटाए जाने और कंटेनर के बनाए रखने के व्यवहार की जाँच करें।
Apple के मौजूदा मार्गदर्शन के अनुसार, lazy stack व्यू के दिखाई देने से पहले उसके बॉडी और लेआउट को प्रीफ़ेच कर सकते हैं। अगर स्क्रॉल की दिशा उलट जाए, तो प्रीफ़ेच किए गए व्यू को शायद कभी onAppear न मिले। Apple यह भी बताता है कि lazy stack की ऑफस्क्रीन स्थिति अंततः हटाई जा सकती है और स्क्रॉल के बाद भी बचा रहना ज़रूरी हो, तो डेटा को मॉडल या बाहरी व्यू में रखने की सलाह देता है। देखें Dive into lazy stacks and scrolling with SwiftUI।
इसलिए उदाहरण की पंक्ति-स्थानीय गिनती एक उपयोगी प्रोब है। असली संपादन इंटरफ़ेस में, जिन बदलावों को पंक्ति हटने के बाद भी बचना चाहिए उन्हें आइटम के स्थिर पहचानकर्ता से जुड़े किसी अधिक समय तक जीवित रहने वाले स्वामी में रखें।
3. जानबूझकर पहचान रीसेट करें
Reset identity क्लिक करें। प्रोग्राम पूरे स्क्रॉल व्यू से जुड़ी पहचान बदलता है, ताकि सामान्य रिविज़न अपडेट से जानबूझकर की गई तुलना की जा सके। Apple स्पष्ट रूप से बताता है कि id(_:) को दी गई वैल्यू बदलने से व्यू की पहचान रीसेट होती है, जिसमें उसकी स्थिति भी शामिल है। देखें id(_:)।
नए दिखाई देने वाले व्यू में नए टोकन और शुरुआती गिनतियाँ देखें। चूँकि रीसेट स्क्रॉल-व्यू सबट्री पर लागू होता है, इसलिए इसे स्क्रॉल-स्थिति बनाए रहने की जाँच के रूप में इस्तेमाल न करें।
अपने ट्रेस की समीक्षा करते समय इस निर्णय-सहायिका का उपयोग करें:
| अवलोकन | समर्थित निष्कर्ष | अगली जाँच |
|---|---|---|
| नए इनिशियलाइज़र संदेश; टोकन और गिनती बनी रहती है | निर्माण हुआ, जबकि निदान स्थिति बनी रही। | अगर अपडेट धीमे हैं तो इनिशियलाइज़र की लागत जाँचें। |
| उपस्थिति कॉलबैक दोहरते हैं; टोकन बना रहता है | बार-बार दिखाई देने से वह टोकन बदला नहीं। | सीमा पर निर्भर व्यवहार के लिए व्यूपोर्ट कॉलबैक इस्तेमाल करें। |
| सामान्य अपडेट के बाद टोकन और गिनती रीसेट हो जाते हैं | पंक्ति की स्थिति अपेक्षा के अनुसार नहीं बची। | डेटा ID, बदलते id मॉडिफ़ायर और शर्त-आधारित शाखाएँ जाँचें। |
| पंक्ति शुरू होती है, लेकिन दिखाई देने की सूचना कभी नहीं देती | केवल निर्माण से दिखाई देना साबित नहीं हुआ। | lazy तैयारी और बाद में स्क्रॉल में हुए बदलावों पर विचार करें। |
| व्यूपोर्ट दृश्यता false हो जाती है | चुनी हुई स्क्रॉल सीमा अब पूरी नहीं होती। | तय करें कि यह सीमा इच्छित व्यवहार के अनुरूप है या नहीं। |
डायनेमिक डेटा के लिए, अपडेट के दौरान पहचानकर्ताओं को उनके मूल आइटम से जुड़ा रखें। हर रीड पर नया पहचानकर्ता बनाने से निरंतरता नहीं रहती। पैरेंट की पहचानें भी जाँचें: स्थिर पंक्ति ID उस सबट्री के भीतर की पंक्ति को नहीं बचा सकती जिसकी पहचान आपने जानबूझकर बदल दी हो। Apple स्थिर डेटा पहचानकर्ताओं और संरचनात्मक पहचान पर Demystify SwiftUI में चर्चा करता है।
पुराने macOS संस्करणों पर समर्थित तरीका क्या है?
नमूने का स्क्रॉल दृश्यता मॉडिफ़ायर macOS 15 माँगता है। डिप्लॉयमेंट टार्गेट कम करने के लिए अलग कार्यान्वयन वाली उपलब्धता शाखा चाहिए; onAppear उसी सीमा का समकक्ष माप नहीं है।
अगर आपका ऐप पहले से AppKit स्क्रॉलिंग इंटरफ़ेस का स्वामी है, तो NSScrollView.documentVisibleRect उसके डॉक्यूमेंट व्यू का वह हिस्सा देता है जो कंटेंट व्यू के माध्यम से दिखाई देता है; यह डॉक्यूमेंट-व्यू निर्देशांकों में व्यक्त होता है। ज्यामिति-आधारित विकल्प यह है कि उस रेक्टैंगल की तुलना उसी निर्देशांक प्रणाली में अपनी पंक्ति के रेक्टैंगल से करें और स्क्रॉल या लेआउट बदलने के बाद फिर से गणना करें। देखें documentVisibleRect।
उदाहरण के लिए, स्पष्ट रूप से चुना गया नियम किसी पंक्ति को दृश्य तब मान सकता है जब उसके आयताकार क्षेत्र का कम से कम आधा हिस्सा डॉक्यूमेंट-दृश्य रेक्टैंगल से intersect करे। यह प्रस्तावित ऐप नियम है, यह दावा नहीं कि इससे SwiftUI का कार्यान्वयन दोहराया जाता है। खाली रेक्टैंगल और निर्देशांक रूपांतरण को स्पष्ट रूप से संभालें।
SwiftUI, आपके ऐप द्वारा बनाए और प्रबंधित किए गए AppKit व्यू को जोड़ने के लिए NSViewRepresentable देता है। इसके दस्तावेज़ीकृत निर्माण, अपडेट, समन्वयन और हटाने के तरीके समर्थित पुल प्रदान करते हैं। दर्शाए गए व्यू के लेआउट पर SwiftUI के नियंत्रण का सम्मान करें; Apple उस व्यू का फ़्रेम या बाउंड सीधे बदलने से सावधान करता है। देखें NSViewRepresentable।
यह वैकल्पिक तरीका आपके स्वामित्व वाले AppKit डॉक्यूमेंट व्यू से जुड़ा है। इसके लिए SwiftUI List के निजी बैकिंग व्यू के बारे में कोई अनुमान लगाने की ज़रूरत नहीं है।
क्या TextKit का लेआउट साबित करता है कि टेक्स्ट स्क्रीन पर है?
TextKit में भी तैयारी और व्यूपोर्ट ज्यामिति के बीच यही अंतर रखना ज़रूरी है। यह तब मायने रखता है जब आपकी अपनी पंक्ति में AppKit टेक्स्ट व्यू हो और सवाल यह बन जाए कि कौन-से टेक्स्ट खंड दृश्य क्षेत्र से intersect करते हैं।
TextKit 2 का NSTextViewportLayoutController, जो macOS 12 और उसके बाद उपलब्ध है, सक्रिय लेआउट क्षेत्र प्रबंधित करता है जिसमें आम तौर पर दृश्य क्षेत्र और ओवरस्क्रॉल क्षेत्र शामिल होते हैं। इसके viewportBounds में ओवरड्रॉ शामिल होता है और इसका वैकल्पिक viewportRange मौजूदा व्यूपोर्ट लेआउट बताता है। इनमें से किसी को भी दृश्यमान अक्षरों की सटीक सूची नहीं कहना चाहिए। देखें NSTextViewportLayoutController।
TextKit 2 कार्यान्वयन में आवंटन घटनाओं के बजाय टेक्स्ट-लेआउट ज्यामिति इस्तेमाल करें। NSTextLayoutManager दिए गए टेक्स्ट रेंज के लिए टेक्स्ट खंडों की गणना कर सकता है। परिणामस्वरूप मिले सेगमेंट रेक्टैंगल की तुलना वास्तविक क्लिपिंग रेक्टैंगल से करें, दोनों को एक ही निर्देशांक प्रणाली में रखने के बाद। टेक्स्ट व्यू के textContainerOrigin का ध्यान रखें; यह न मानें कि टेक्स्ट-कंटेनर निर्देशांक डॉक्यूमेंट-व्यू निर्देशांकों के बराबर हैं। देखें enumerateTextSegments(in:type:options:using:) और textContainerOrigin।
मौजूदा TextKit 1 कार्यान्वयन के लिए, NSLayoutManager.glyphRange(forBoundingRect:in:) टेक्स्ट-कंटेनर निर्देशांकों में एक रेक्टैंगल स्वीकार करता है। Apple बताता है कि लौटाई गई सन्निहित रेंज में उस रेक्टैंगल के बाहर के ग्लिफ़ भी शामिल हो सकते हैं, इसलिए यह आगे जाँचने के लिए संभावित रेंज है, दृश्यता की सटीक सूची नहीं। देखें glyphRange(forBoundingRect:in:)।
जाँच करते समय टेक्स्ट इंजन को उसी रूप में बनाए रखें। Apple के दस्तावेज़ों के अनुसार, macOS 12 और उसके बाद NSTextView या टेक्स्ट कंटेनर की layoutManager प्रॉपर्टी तक पहुँचने से NSLayoutManager इस्तेमाल करने वाले कम्पैटिबिलिटी मोड में फ़ॉलबैक होता है। TextKit 2 पथ के लिए textLayoutManager और उसके API इस्तेमाल करें। देखें NSTextView।
इंटरफ़ेस के सवाल का जवाब देने वाला संकेत चुनें: निर्माण के लिए इनिशियलाइज़र लॉग, निरंतरता के लिए बनाए रखी गई स्थिति, दिखाई देने के बदलावों के लिए उपस्थिति कॉलबैक और दृश्यता के लिए व्यूपोर्ट ज्यामिति। स्थिर ID वाला प्रयोग और जानबूझकर किया गया एक पहचान रीसेट, कंसोल आउटपुट की मात्रा से अनुमान लगाए बिना इन अंतरों को दिखाते हैं।
स्रोत
- Swift में वैल्यू और रेफ़रेंस टाइप — Swift.org
- Demystify SwiftUI — Apple
onAppear(perform:)— AppleonDisappear(perform:)— AppleonScrollVisibilityChange(threshold:_:)— AppleLazyVStack— AppleState— Apple- Dive into lazy stacks and scrolling with SwiftUI — Apple
id(_:)— AppledocumentVisibleRect— AppleNSViewRepresentable— AppleNSTextViewportLayoutController— AppleenumerateTextSegments(in:type:options:using:)— AppletextContainerOrigin— AppleglyphRange(forBoundingRect:in:)— AppleNSTextView— Apple