अपने SwiftUI कंपोनेंट को प्रोटोकॉल के अनुरूप प्रकार के लिए जेनेरिक बनाएँ, फिर उसके स्टोर किए गए व्यू को Provider.Content के रूप में घोषित करें। इससे पूरे कंपोनेंट में प्रोवाइडर और उसके संबद्ध व्यू प्रकार के बीच संबंध बना रहता है।
यह ट्यूटोरियल एक छोटी macOS विंडो बनाता है जिसमें दो कृत्रिम टाइलें हैं। इसमें काम यह है कि प्रोवाइडर और उससे बनने वाले व्यू को any View या AnyView से संबद्ध प्रकार को बदले बिना स्टोर किया जाए। यही पैटर्न तब भी लागू होता है जब कोई पुनः उपयोग योग्य डेस्कटॉप कंपोनेंट कस्टम लेबल, एक्सेसरी या कंटेंट प्रोवाइडर स्वीकार करता है।
स्टोर की गई प्रॉपर्टीज़ को कौन-सा संबंध बनाए रखना चाहिए?
संबद्ध प्रकार उस प्रकार का नाम है जिसे हर प्रोटोकॉल अनुरूपता चुनती है। यदि कोई प्रोटोकॉल associatedtype Content: View घोषित करता है, तो अनुरूप प्रोवाइडर को एक विशिष्ट कंटेंट प्रकार देना होगा। जेनेरिक कोड इस प्रकार को Provider.Content के रूप में संदर्भित कर सकता है, भले ही उसका कार्यान्वयन किसी अपारदर्शी परिणाम के पीछे छिपा हो। Swift संबद्ध प्रकारों और जेनेरिक बाधाओं का वर्णन अपने Generics अध्याय में करता है।
इसलिए कंपोनेंट को दो जुड़ी हुई घोषणाओं की ज़रूरत है: प्रोवाइडर प्रोटोकॉल से बँधा एक जेनेरिक पैरामीटर, और एक स्टोर की गई कंटेंट प्रॉपर्टी जिसका प्रकार उस पैरामीटर का संबद्ध प्रकार हो। केवल प्रोटोकॉल के अनुरूप होने वाली दो स्वतंत्र प्रॉपर्टीज़ घोषित करने से यह संबंध व्यक्त नहीं होता।
SwiftUI स्वयं भी इससे मिलता-जुलता अनुबंध इस्तेमाल करता है: View में associatedtype Body: View घोषित है, और Swift कस्टम व्यू के body कार्यान्वयन से इस संबद्ध प्रकार का अनुमान लगाता है। आपके प्रोवाइडर का Content और कंपोनेंट का Body अलग-अलग संबद्ध प्रकार हैं; उनका समान होना ज़रूरी नहीं है। Apple का Body दस्तावेज़ीकरण देखें।
पूरा macOS उदाहरण बनाएँ
इस उदाहरण के लिए SwiftUI और macOS 13 या बाद के डिप्लॉयमेंट टार्गेट वाला Xcode macOS ऐप प्रोजेक्ट इस्तेमाल करें। जेनरेट की गई ऐप और कंटेंट-व्यू घोषणाओं की जगह नीचे दी गई एक फ़ाइल रखें और टार्गेट में ठीक एक @main घोषणा रखें। Apple इस ऐप-एंट्री संरचना और WindowGroup के उपयोग का वर्णन App में करता है।
यह उदाहरण जानबूझकर लंबे समय से उपलब्ध संबद्ध-प्रकार और नामित-जेनेरिक सिंटैक्स इस्तेमाल करता है। अपारदर्शी परिणाम Swift 5.1 में लागू किए गए थे; आगे चर्चा किया गया वैकल्पिक प्राथमिक-संबद्ध-प्रकार सिंटैक्स Swift 5.7 में उपलब्ध हुआ। पूरी फ़ाइल मौजूदा Swift 6 टूलचेन के लिए है। इसे प्राप्त दस्तावेज़ों के आधार पर जाँचा गया, लेकिन यहाँ कंपाइल नहीं किया गया क्योंकि इस वातावरण में Swift कंपाइलर उपलब्ध नहीं है। चुना गया macOS डिप्लॉयमेंट टार्गेट उदाहरण की पूर्वापेक्षा है; यह दावा नहीं है कि हर अंतर्निहित सुविधा के समर्थन वाला यही सबसे पुराना OS है। SE-0244 और SE-0346 देखें।
import SwiftUI
@MainActor
protocol TileProvider {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
@MainActor
struct TextTile: TileProvider {
let title: String
let message: String
func makeContent() -> Text {
Text(message)
}
}
@MainActor
struct CircleTile: TileProvider {
let title: String
let caption: String
func makeContent() -> some View {
HStack(spacing: 8) {
Circle()
.fill(Color.blue)
.frame(width: 16, height: 16)
Text(caption)
}
}
}
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
private let content: Provider.Content
init(provider: Provider) {
self.provider = provider
self.content = provider.makeContent()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
content
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
@MainActor
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
TileCard(provider: TextTile(
title: "Text tile",
message: "Blue square"
))
TileCard(provider: CircleTile(
title: "Shape tile",
caption: "Blue circle"
))
}
.padding()
.frame(width: 360)
}
}
@main
@MainActor
struct AssociatedViewDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
macOS टार्गेट को बिल्ड करके चलाएँ। अपेक्षित परिणाम एक विंडो है जिसमें एक टेक्स्ट टाइल और कैप्शन के बगल में नीले वृत्त वाली एक टाइल होगी। दोनों एक ही जेनेरिक कंपोनेंट इस्तेमाल करते हैं, और हर कॉल साइट पर प्रोवाइडर प्रकार का अलग-अलग अनुमान लगाया जाता है।
स्पष्ट @MainActor एनोटेशन इन UI बनाने वाली घोषणाओं को एक ही ऐक्टर संदर्भ में रखती हैं। Apple का मौजूदा View दस्तावेज़ीकरण प्रोटोकॉल को मेन-ऐक्टर आइसोलेशन के साथ घोषित करता है। यहाँ दिए गए एनोटेशन कस्टम प्रोवाइडर प्रोटोकॉल के इच्छित संदर्भ को भी स्पष्ट करते हैं।
जेनेरिक कंपोनेंट प्रकार को क्यों बनाए रखता है?
पहले कार्ड के लिए Provider का प्रकार TextTile है, इसलिए Provider.Content का प्रकार Text है। दूसरे कार्ड के लिए Provider का प्रकार CircleTile है, और उसका संबद्ध कंटेंट प्रकार CircleTile.makeContent() का अपारदर्शी परिणाम है। आप नेस्टेड SwiftUI लेआउट प्रकार लिखे बिना CircleTile.Content के ज़रिए इस प्रकार का नाम ले सकते हैं।
मुख्य असाइनमेंट self.content = provider.makeContent() है। प्रोटोकॉल गारंटी देता है कि यह मेथड ठीक Provider.Content लौटाता है, जो स्टोर की गई प्रॉपर्टी का घोषित प्रकार भी है। यह जेनेरिक बाधा से निकला कंपाइल-टाइम संबंध है, रनटाइम प्रकार की तुलना नहीं। Swift का Generics अध्याय बताता है कि बाधित जेनेरिक पैरामीटर के ज़रिए संबद्ध प्रकारों तक कैसे पहुँचें।
अनुरूप कार्यान्वयन में some View इस्तेमाल करने से यह संबंध मिटता नहीं है। अपारदर्शी परिणाम ठोस कार्यान्वयन को छिपाते हुए उसकी प्रकार-पहचान बनाए रखता है। इसलिए इसे संबद्ध-प्रकार के प्रमाण के रूप में इस्तेमाल किया जा सकता है। Swift इस अंतर का वर्णन Opaque and Boxed Protocol Types में करता है।
इन कार्डों के अलग-अलग स्पेशलाइज़ेशन हैं: TileCard<TextTile> और TileCard<CircleTile>। स्रोत घोषणा का पुनः उपयोग करने के लिए यह ज़रूरी नहीं कि हर कार्ड का स्टोर किया हुआ कंटेंट प्रकार एक ही सार्वभौमिक प्रकार हो।
स्वतंत्र अपारदर्शी या एक्सिस्टेंशियल प्रॉपर्टीज़ से काम क्यों नहीं चलता?
नीचे दिए गए अंश केवल त्रुटि समझाने के लिए हैं; ये पूरे प्रोग्राम के विकल्प नहीं हैं:
// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View
// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider
// Diagnostic fragment: stores an existential view value.
let content: any View
किसी अपारदर्शी स्टोर की गई प्रॉपर्टी को उसके अंतर्निहित प्रकार को स्थापित करने वाली घोषणा-समय की इनिशियलाइज़र चाहिए। some View लिखने से कॉलर द्वारा चुना गया कोई जेनेरिक पैरामीटर नहीं बनता जिसे कंपोनेंट का मनमाना इनिशियलाइज़र बाद में भर सके। उदाहरण के लिए, let content: some View = Text("Sample") उस घोषणा के लिए एक प्रकार स्थापित करता है; यह प्रॉपर्टी को किसी दूसरी प्रॉपर्टी के संबद्ध प्रकार से नहीं जोड़ता। स्टोर की गई प्रॉपर्टीज़ का यह नियम SE-0244: Opaque Result Types में दर्ज है।
any TileProvider जैसा एक्सिस्टेंशियल अलग-अलग अनुरूप प्रोवाइडर प्रकारों को स्टोर करने देता है। यह अनुरूप प्रकार को छिपा देता है, इसलिए घोषणा अब Provider.Content के रूप में संबंध नहीं दिखाती। इसी तरह, any View एक एक्सिस्टेंशियल कंटेनर है; यह View के अनुरूप नामित जेनेरिक प्रकार वाले अनुबंध के समान नहीं है। Swift की boxed protocol types संबंधी व्याख्या देखें।
कुछ जेनेरिक कॉल के लिए Swift एक्सिस्टेंशियल आर्ग्युमेंट को परोक्ष रूप से खोल सकता है। इससे हर एक्सिस्टेंशियल हर जेनेरिक इनिशियलाइज़र में उपयोग योग्य नहीं हो जाता। विशेष रूप से, SE-0352 उस स्थिति में खोलने को सीमित करता है जहाँ अज्ञात अंतर्निहित प्रकार X<T> जैसे जेनेरिक परिणाम के भीतर बाहर निकलता हो। जेनेरिक कंपोनेंट स्पेशलाइज़ेशन ठीक ऐसी सीमा पर पहुँच सकता है। एक्सिस्टेंशियल को हमेशा वापस स्थायी स्पेशलाइज़ेशन में बदला जा सकेगा, यह मानने के बजाय कार्ड बनाने से पहले प्रोवाइडर को जेनेरिक रखें। SE-0352: Implicitly Opened Existentials देखें।
कंटेंट कब बनना चाहिए, उसके अनुसार स्टोरेज चुनें
उदाहरण हर TileCard इनिशियलाइज़ेशन के दौरान makeContent() कॉल करता है और लौटे हुए व्यू मान को स्टोर करता है। यह बाद के कंपोनेंट इनिशियलाइज़ेशन में भी बने रहने वाला कैश स्थापित नहीं करता। दोनों प्रॉपर्टीज़ रखना अनुरोधित प्रकार-संबंध दिखाता है, लेकिन जिस कंपोनेंट को केवल तैयार व्यू चाहिए, वह स्टोर किया गया प्रोवाइडर हटा सकता है।
यदि कंपोनेंट को हर बार body का मूल्यांकन होने पर कंटेंट माँगना चाहिए, तो केवल प्रोवाइडर रखें। नीचे दिया गया कंपोनेंट डिक्लेरेशन, ऊपर के प्रोटोकॉल और ऐप के साथ इस्तेमाल करने के लिए उसका विकल्प है:
@MainActor
struct TileCard<Provider: TileProvider>: View {
private let provider: Provider
init(provider: Provider) {
self.provider = provider
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(provider.title)
.font(.headline)
provider.makeContent()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.12))
}
}
दोनों डिज़ाइन संबद्ध प्रकार बनाए रखते हैं। उनका अंतर सीधे इस बात से तय होता है कि मेथड कॉल कहाँ है: पहले डिज़ाइन में इनिशियलाइज़ेशन के दौरान, दूसरे में बॉडी के मूल्यांकन के दौरान। इस समय का सोच-समझकर चुनाव करें, और कंटेंट बनाने वाले काम को ऐसा रखें जो ठीक एक बार बुलाए जाने पर निर्भर न हो।
उदाहरण में बदलाव करते समय इस निर्णय-सहायिका का उपयोग करें:
| कंपोनेंट की ज़रूरत | घोषणा की रणनीति | प्रकार का संबंध |
|---|---|---|
| प्रोवाइडर और उससे बना व्यू स्टोर करना | जेनेरिक प्रोवाइडर के साथ Provider.Content |
प्रोवाइडर की पहचान और उसका कंटेंट प्रकार, दोनों बनाए रखता है |
| बॉडी के मूल्यांकन के दौरान कंटेंट बनाना | जेनेरिक प्रोवाइडर स्टोर करें | अलग कंटेंट मान स्टोर किए बिना संबद्ध प्रकार बनाए रखता है |
| ज्ञात साझा कंटेंट प्रकार वाले प्रोवाइडर स्टोर करना | बाधित एक्सिस्टेंशियल पर विचार करें | प्रोवाइडर की पहचान छिपाते हुए निर्दिष्ट कंटेंट समानता बनाए रखता है |
| अलग-अलग व्यू कार्यान्वयनों को जानबूझकर मिटाना | चुनी हुई सीमा पर AnyView इस्तेमाल करें |
मूल व्यू प्रकार के बजाय रैपर प्रकार स्टोर करता है |
क्या प्राथमिक संबद्ध प्रकार जेनेरिक की ज़रूरत खत्म कर देते हैं?
प्राथमिक संबद्ध प्रकार चुनी हुई समान-प्रकार बाधाएँ व्यक्त करने का छोटा तरीका देते हैं। यदि आप प्रोटोकॉल के स्वामी हैं, तो यह वैकल्पिक हेडर Content को प्राथमिक बनाता है; बाकी प्रोटोकॉल आवश्यकताएँ वैसी ही रहती हैं:
// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
associatedtype Content: View
var title: String { get }
func makeContent() -> Content
}
इस घोषणा के साथ, any TileProvider<Text> ऐसे एक्सिस्टेंशियल का वर्णन करता है जिसका संबद्ध कंटेंट प्रकार Text होना ज्ञात है। अलग-अलग प्रोवाइडर कार्यान्वयन इस स्टोरेज प्रकार को तभी साझा कर सकते हैं जब उनकी अनुरूपताएँ ठीक उसी कंटेंट समानता को उजागर करें। यह सिंटैक्स एक संबद्ध प्रकार पर बाधा लगाता है; इससे प्रोटोकॉल जेनेरिक स्ट्रक्ट नहीं बनता। SE-0346 और SE-0353 देखें।
इससे मनमाने अज्ञात व्यू प्रकार एक ही संरक्षित ठोस प्रकार में नहीं जुड़ जाते। खास तौर पर, CircleTile अपारदर्शी कंटेंट परिणाम उजागर करता है, इसलिए कॉलर उस परिणाम को सीधे Text नहीं मान सकते। यदि प्रोवाइडर के संबद्ध कंटेंट पर कोई रोक नहीं होनी चाहिए, तो मूल जेनेरिक कंपोनेंट इस्तेमाल करें।
बाधित एक्सिस्टेंशियल Swift 5.7 में लागू किए गए थे, और SE-0353 रनटाइम तथा पुराने डिप्लॉयमेंट के साथ संगतता की सीमाएँ स्पष्ट रूप से बताता है। इसलिए केवल कंपाइलर समर्थन से उस वैकल्पिक डिज़ाइन की डिप्लॉयमेंट संगतता सिद्ध नहीं होती। उसे अपनाने से पहले टार्गेट टूलचेन के उपलब्धता डायग्नॉस्टिक्स जाँचें। मुख्य उदाहरण बाधित एक्सिस्टेंशियल स्टोरेज पर निर्भर नहीं है। SE-0353 की ABI चर्चा देखें।
कंपाइलेशन विफल होने पर क्या जाँचें?
लेआउट बदलने से पहले प्रोटोकॉल की आवश्यकता से स्टोर की गई प्रॉपर्टी तक प्रकार का पता लगाएँ:
- पुष्टि करें कि प्रोटोकॉल में
Viewसे बाधित संबद्ध प्रकार घोषित है। - पुष्टि करें कि कंपोनेंट में उस प्रोटोकॉल से बँधा नामित जेनेरिक प्रोवाइडर पैरामीटर है।
- स्टोर की गई कंटेंट प्रॉपर्टी को
Provider.Contentघोषित करें, जो मेथड के रिटर्न प्रकार से मेल खाता हो। - जाँचें कि कहीं पैरेंट ने प्रोवाइडर को पहले ही
any TileProviderमें तो नहीं बदला है। - यदि स्टोरेज से पहले मॉडिफ़ायर जोड़ रहे हैं, तो उसका परिणाम प्रकार जाँचें: संशोधित व्यू अब
Provider.Contentनहीं हो सकता। - ऐक्टर-आइसोलेशन डायग्नॉस्टिक्स को संबद्ध-प्रकार डायग्नॉस्टिक्स से अलग जाँचें।
Apple के अनुसार व्यू मॉडिफ़ायर किसी व्यू को दूसरे व्यू में लपेटते हैं। इसलिए Provider.Content घोषित प्रॉपर्टी में provider.makeContent().padding() असाइन करने से असाइनमेंट को पूरा करने वाला अनुबंध बदल जाता है। मूल कंटेंट स्टोर करें और प्रेज़ेंटेशन मॉडिफ़ायर body में लागू करें, या स्टोर किए गए प्रकार को संशोधित परिणाम के अनुरूप फिर से डिज़ाइन करें। View देखें।
बिल्डर के लिए भी संस्करण की सीमा है। Apple का मौजूदा दस्तावेज़ीकरण Xcode 26 और उससे पहले के लिए ViewBuilder के व्यवहार का वर्णन करता है, फिर Xcode 27 से शुरू होने वाले कंटेंट-बिल्डिंग के अलग व्यवहार का। इस उदाहरण में कस्टम बिल्डर आवश्यकता से बचा गया है: हर प्रोवाइडर एक स्पष्ट एक्सप्रेशन लौटाता है। यदि आप इसे बिल्डर-आधारित प्रोवाइडर API के साथ बढ़ाते हैं, तो वास्तव में इस्तेमाल किए जा रहे Xcode संस्करण के दस्तावेज़ों का पालन करें। ViewBuilder देखें।
यदि आप जानबूझकर AnyView चुनते हैं, तो उसके दस्तावेज़ित व्यवहार को ध्यान में रखें: लिपटे हुए व्यू का प्रकार बदलने पर पुराना हाइरार्की नष्ट हो जाता है और नया बनता है। यह स्टोरेज में संबद्ध प्रकार बनाए रखने से अलग डिज़ाइन निर्णय है। Apple इसे AnyView में दर्ज करता है।
इस कंपोनेंट के लिए स्टोरेज सीमा पर संबंध को स्पष्ट रखें: एक जेनेरिक Provider, Provider.Content का परिणाम, और उसी प्रकार की स्टोर की गई प्रॉपर्टी। फिर प्रोवाइडर का आंतरिक लेआउट बदलते हुए कंपाइलर को उसका संबद्ध व्यू प्रकार कंपोनेंट में आगे ले जाने दें।
स्रोत
- Generics — Swift.org
- Opaque and Boxed Protocol Types — Swift.org
- View — Apple
- Body — Apple
- App — Apple
- ViewBuilder — Apple
- AnyView — Apple
- SE-0244: Opaque Result Types — Swift Evolution
- SE-0346: Lightweight same-type requirements for primary associated types — Swift Evolution
- SE-0352: Implicitly Opened Existentials — Swift Evolution
- SE-0353: Constrained Existential Types — Swift Evolution