ओवरलैप करती हुई व्यू में पारदर्शी छेद के आर-पार SwiftUI कंट्रोल पर क्लिक करने के लिए, ऊपरी व्यू को ऐसा स्पष्ट हिट-टेस्ट आकार दें जो छेद को बाहर रखे। यह वॉकथ्रू एक ही Mac ऐप के भीतर इस इंटरैक्शन को बनाता है, जिसमें मेमोरी में बना एक रिंग और उसके नीचे एक बटन है।

ठोस कार्य एक छोटा डेस्कटॉप ड्रॉइंग इंटरफ़ेस है: नारंगी रिंग पर क्लिक करने से रिंग चुनी जाती है; उसके पारदर्शी केंद्र पर क्लिक करने से नीचे दिख रहा नीला कंट्रोल सक्रिय होता है। दोनों परतें एक ही SwiftUI व्यू पदानुक्रम में हैं। उदाहरण ड्रॉइंग और हिट टेस्टिंग के लिए साझा ज्यामिति का उपयोग करता है, इसलिए इसे किसी इमेज फ़ाइल या पिक्सेल सैंपलिंग की ज़रूरत नहीं है।

इवेंट हैंडलिंग बदलने से पहले हिट-टेस्ट नियम चुनें

Apple SwiftUI के दो प्रासंगिक कंट्रोल दस्तावेज़ित करता है। allowsHitTesting(_:) तय करता है कि कोई व्यू हिट टेस्टिंग में भाग लेगी या नहीं। contentShape(_:eoFill:) हिट टेस्टिंग के लिए उपयोग होने वाला आकार निर्धारित करता है। पहले का उपयोग ऐसे सजावटी तत्वों के लिए करें जिन्हें कभी क्लिक न मिले; दूसरे का उपयोग छेद वाले इंटरैक्टिव ओवरले के लिए करें। हिट-टेस्टिंग में भागीदारी पर Apple का दस्तावेज़ और कंटेंट-शेप का दस्तावेज़ इन अलग-अलग ज़िम्मेदारियों का वर्णन करते हैं।

ऊपरी परत की आवश्यकता कार्यान्वयन का विकल्प
सजावटी तत्व हर क्लिक को अनदेखा करे उस सजावटी तत्व पर .allowsHitTesting(false) लागू करें
वृत्त अपनी सीमा के भीतर प्रतिक्रिया दे उसके इंटरैक्टिव व्यू को वृत्ताकार कंटेंट-शेप दें
रिंग पारदर्शी केंद्र के चारों ओर प्रतिक्रिया दे ड्रॉइंग और हिट टेस्टिंग, दोनों के लिए संयुक्त आकार और इवन-ऑड फिलिंग का उपयोग करें
अपने नियंत्रण में मौजूद AppKit सबट्री को चुनिंदा रूप से भाग लेना हो उसकी hitTest(_:) सीमा ओवरराइड करें और इंटरैक्शन क्षेत्र के बाहर के बिंदुओं को अस्वीकार करें

ये API भागीदारी और ज्यामिति बताते हैं; ये अंतिम रेंडर किए गए पिक्सेल की अल्फ़ा का स्वतः परीक्षण निर्धारित नहीं करते। सिंथेटिक आकारों के लिए समर्थित तरीका यह है कि इच्छित इंटरैक्शन क्षेत्र स्पष्ट रूप से बताया जाए। इसलिए केवल पारदर्शी दिखना, इंटरैक्शन के लिए अधूरा विवरण है। यह अंतर दस्तावेज़ित बूलियन और आकार-आधारित इंटरफ़ेस से निकलता है, हर रेंडरिंग मॉडिफ़ायर के बारे में किसी वादे से नहीं।

रिंग और बटन वाला पूरा SwiftUI उदाहरण बनाएं

Xcode में SwiftUI का उपयोग करके macOS ऐप बनाएं और उसका डिप्लॉयमेंट टार्गेट macOS 12 या उसके बाद का सेट करें। जनरेट की गई ऐप डिक्लेरेशन और शुरुआती व्यू को नीचे दिए पूरे प्रोग्राम से बदलें; टार्गेट में केवल एक @main डिक्लेरेशन रखें। उदाहरण में .contentShape(.interaction, …) का उपयोग है, जिसका इंटरैक्शन प्रकार macOS 12 से उपलब्ध है और जो हिट टेस्टिंग को स्पष्ट रूप से नियंत्रित करता है। इंटरैक्शन पर Apple का दस्तावेज़ इस व्यवहार और उपलब्धता की जानकारी देता है।

रिंग में दो संकेंद्रित दीर्घवृत्ताकार सबपाथ हैं। ड्रॉइंग और इंटरैक्शन, दोनों इवन-ऑड नियम का उपयोग करते हैं, जिससे भीतरी दीर्घवृत्त छेद बन जाता है। Apple Shape.path(in:) को दिए गए आयत के भीतर की ज्यामिति के रूप में और FillStyle को रेंडरिंग कॉन्फ़िगरेशन के रूप में दस्तावेज़ित करता है, जिसमें इवन-ऑड फिलिंग भी शामिल है। शेप ज्यामिति और FillStyle इनके मूल अनुबंध बताते हैं।

import SwiftUI

struct RingShape: Shape {
    func path(in rect: CGRect) -> Path {
        let side = min(rect.width, rect.height)
        let outer = CGRect(
            x: rect.midX - side / 2,
            y: rect.midY - side / 2,
            width: side,
            height: side
        )
        let inner = outer.insetBy(
            dx: side * 0.30,
            dy: side * 0.30
        )

        var path = Path()
        path.addEllipse(in: outer)
        path.addEllipse(in: inner)
        return path
    }
}

struct RoutingDemo: View {
    @State private var lowerClicks = 0
    @State private var ringClicks = 0

    var body: some View {
        VStack(spacing: 18) {
            Text("Click the center or the orange ring")

            ZStack {
                Button {
                    lowerClicks += 1
                } label: {
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: 240, height: 240)
                        .overlay {
                            Text("Below")
                                .foregroundColor(.white)
                        }
                        .contentShape(Rectangle())
                }
                .buttonStyle(.plain)

                RingShape()
                    .fill(Color.orange, style: FillStyle(eoFill: true))
                    .frame(width: 240, height: 240)
                    .contentShape(.interaction, RingShape(), eoFill: true)
                    .onTapGesture {
                        ringClicks += 1
                    }

                Rectangle()
                    .stroke(Color.gray, lineWidth: 1)
                    .frame(width: 240, height: 240)
                    .allowsHitTesting(false)
            }
            .frame(width: 240, height: 240)

            Text("Below: \(lowerClicks) | Ring: \(ringClicks)")
        }
        .padding(24)
    }
}

@main
struct TransparentRoutingApp: App {
    var body: some Scene {
        WindowGroup {
            RoutingDemo()
        }
    }
}

नीचे वाला बटन अपनी कार्रवाई के लिए ज़िम्मेदार रहता है। रिंग का जेस्चर एक अलग काउंटर बढ़ाता है। ये ट्रेलिंग क्लोज़र दोनों कार्रवाइयों को सीधे व्यक्त करते हैं; Swift की भाषा मार्गदर्शिका क्लोज़र को ऐसी कार्यक्षमता के रूप में बताती है जो आसपास की वैल्यू कैप्चर कर सकती है। The Swift Programming Language: Closures सिंटैक्स और कैप्चर व्यवहार समझाता है।

धूसरी आउटलाइन सजावटी है, इसलिए केवल उसी की हिट टेस्टिंग बंद है। यह मॉडिफ़ायर आउटलाइन पर ही रखें: इंटरैक्टिव परतों वाले कंटेनर पर इसे लगाने से कंटेनर की भागीदारी नीति गलत हो जाएगी। रिंग स्वयं नारंगी पट्टी पर इंटरैक्शन के लिए उपलब्ध रहनी चाहिए।

एक ही फिल नियम दो बार क्यों मायने रखता है

ड्रॉइंग कॉल और कंटेंट-शेप मॉडिफ़ायर, दोनों को eoFill: true मिलता है। इसे केवल ड्रॉइंग के लिए सेट करने से दिखने वाला छेद तो बताया जाएगा, लेकिन हिट टेस्टिंग को वही नियम स्पष्ट रूप से नहीं मिलेगा। Apple कंटेंट-शेप पैरामीटर को रेंडरिंग से अलग दस्तावेज़ित करता है, इसलिए इस उदाहरण में दोनों घोषणाएँ ज़रूरी हैं। कंटेंट-शेप मॉडिफ़ायर इंटरैक्शन आकार और उसका वाइंडिंग नियम निर्धारित करता है।

आयामों और रूपांतरणों को भी एक जैसा रखें। यहाँ रिंग की ड्रॉइंग और कंटेंट-शेप, दोनों 240-पॉइंट के वर्ग का उपयोग करते हैं। यदि बाद में आप पूरी रिंग व्यू को खिसकाएँ या स्केल करें, तो उन कार्रवाइयों को साथ रखें। यदि इसके बजाय आप किसी बड़े कंटेनर के भीतर केवल ड्रॉइंग पाथ बदलते हैं, तो इंटरैक्शन ज्यामिति को भी इच्छित क्षेत्र के अनुरूप अपडेट करें।

रूटिंग को तीन खास क्लिक से सत्यापित करें

ऐप विंडो सक्रिय करें, फिर आकार की सीमाओं से काफी दूर क्लिक करें। नीचे दिए गए बिंदु 240-पॉइंट वर्ग के भीतर के उदाहरणात्मक स्थानीय निर्देशांक हैं, जिन्हें उसके ऊपरी-बाएँ कोने से मापा गया है। ये एंटीएलियस्ड किनारे के पिक्सेल पर निर्भर हुए बिना केंद्र के छेद, नारंगी पट्टी और पारदर्शी कोने में अंतर करते हैं।

बिंदु ऊपरी परत की दिखाई देने वाली ज्यामिति काउंटर में अपेक्षित बदलाव
(120, 120) केंद्र का छेद Below एक बार बढ़ता है
(210, 120) नारंगी पट्टी Ring एक बार बढ़ता है
(10, 10) बाहरी वृत्त के बाहर Below एक बार बढ़ता है

बाहरी त्रिज्या 120 पॉइंट है। भीतरी इनसेट हर तरफ 72 पॉइंट है, जिससे भीतरी त्रिज्या 48 पॉइंट रह जाती है। तीन परीक्षण बिंदु केंद्र से क्रमशः 0, 90 और लगभग 155.6 पॉइंट दूर हैं। मैंने Python से इस नियतात्मक ज्यामिति गणना की जाँच की; SwiftUI ऐप का निरीक्षण किया, लेकिन इस परिवेश में इसे संकलित या चलाया नहीं जा सका क्योंकि Swift टूलचेन उपलब्ध नहीं था।

यह परीक्षण एक उपयोगी निदान क्रम भी है। यदि नारंगी पट्टी प्रतिक्रिया नहीं देती, तो रिंग के जेस्चर और भागीदारी मॉडिफ़ायर देखें। यदि केंद्र रिंग को सक्रिय करता है, तो उसका कंटेंट-शेप और इवन-ऑड पैरामीटर देखें। यदि छेद में कोई भी परत प्रतिक्रिया नहीं देती, तो अतिरिक्त ओवरले अस्थायी रूप से हटाएँ, फिर उन्हें एक-एक करके वापस जोड़ें ताकि पता चले कि कौन-सी परत रूटिंग बदलती है।

अपने कोड में ज्यामिति की सीधी जाँच के लिए Path.contains(_:eoFill:) भी यही इवन-ऑड विकल्प देता है। पाथ बनाते समय उपयोग किए गए निर्देशांक तंत्र में व्यक्त बिंदु दें। इससे ज्यामिति परीक्षण स्क्रीनशॉट को इंटरैक्शन मॉडल माने बिना वही अंदर-बाहर का प्रश्न पूछ सकता है। पाथ-कंटेनमेंट पर Apple का दस्तावेज़ इस कार्रवाई का वर्णन करता है।

AppKit हिट टेस्टिंग का उपयोग केवल अपनी नियंत्रण सीमा पर करें

जब कोई ओवरले पहले से AppKit में लागू हो, तो NSView.hitTest(_:) दस्तावेज़ित कस्टमाइज़ेशन पॉइंट है। यह दिए गए बिंदु के लिए चुनी गई चाइल्ड व्यू लौटाता है और मुख्य रूप से माउस-डाउन प्राप्त करने वाली व्यू चुनने के लिए उपयोग होता है। इसका इनपुट बिंदु रिसीवर के सुपरव्यू निर्देशांकों में होता है, इसलिए स्थानीय ज्यामिति से तुलना करने से पहले उसे कन्वर्ट करें। NSView हिट-टेस्ट पर Apple का दस्तावेज़ ये विवरण निर्दिष्ट करता है।

अपने नियंत्रण में मौजूद कस्टम व्यू के लिए, ओवरराइड इंटरैक्शन क्षेत्र के बाहर के बिंदुओं को अस्वीकार कर सकता है और स्वीकार किए गए बिंदुओं को super.hitTest(_:) तक पहुँचा सकता है। नीचे का अंश किसी मौजूदा कस्टम NSView सबक्लास की मेथड है, दूसरा पूरा ऐप नहीं। यह ऊपर परिभाषित RingShape का उपयोग करता है और import SwiftUI के साथ import AppKit भी आवश्यक है।

override func hitTest(_ point: NSPoint) -> NSView? {
    guard let parent = superview else {
        return super.hitTest(point)
    }

    let localPoint = convert(point, from: parent)
    let region = RingShape().path(in: bounds)

    guard region.contains(localPoint, eoFill: true) else {
        return nil
    }

    return super.hitTest(point)
}

इस अंश का उपयोग केवल वहाँ करें जहाँ कस्टम व्यू का दिखने वाला कंटेंट और उसकी सबट्री वास्तव में उसी रिंग ज्यामिति का पालन करते हों। nil लौटाने से उस बिंदु के लिए सबट्री अस्वीकार होती है; इससे कोई डेस्टिनेशन तय नहीं होता और न ही SwiftUI को किसी खास सिब्लिंग को सक्रिय करने का निर्देश मिलता है। इच्छित निचली परत क्लिक प्राप्त कर सकती है या नहीं, यह आसपास के पदानुक्रम पर निर्भर करता है। यदि कोई घेरने वाली व्यू उस बिंदु को स्वीकार करती रहती है, तो वह बाधा बनी रह सकती है।

उस अपने नियंत्रण वाले AppKit व्यू को SwiftUI में एम्बेड करते समय, उसे बनाने और उसका कॉन्फ़िगरेशन अपडेट करने के लिए NSViewRepresentable का उपयोग करें। रिप्रेज़ेंटेबल के अपडेट मेथड के माध्यम से आइटम स्टेट और इंटरैक्शन ज्यामिति को सिंक्रोनाइज़ रखें; डेलीगेट या टार्गेट-एक्शन संदेशों को ब्रिज करने के लिए कोऑर्डिनेटर का उपयोग करें। Apple यह भी निर्देश देता है कि रिप्रेज़ेंट की गई रूट व्यू के frame और bounds को SwiftUI के लेआउट नियंत्रण में ही रहने दें। NSViewRepresentable पर Apple का दस्तावेज़ इन जीवनचक्र और स्वामित्व सीमाओं का वर्णन करता है।

मूल इवेंट और जेस्चर के स्वामी को बनाए रखें

रूटिंग का निर्णय हिट टेस्टिंग के दौरान करें। ऊपरी परत का क्लिक पकड़कर नीचे वाले कंट्रोल की कार्रवाई को मैन्युअल रूप से कॉल करने से बचें: इससे निचले कंट्रोल का सामान्य इंटरैक्शन बदलकर ऐप का कमांड लागू होगा। इसी तरह, इस उदाहरण में इवेंट को दोबारा भेजने, SwiftUI की कार्यान्वयन व्यूज़ में खोज करने या निचली परत के जेस्चर हैंडलर को बदलने की ज़रूरत नहीं है। हर दिखाई देने वाली इंटरैक्टिव परत अपने व्यवहार की स्वामी है।

यहाँ दायरा ऐप के व्यू पदानुक्रम के भीतर सामान्य प्राथमिक-बटन सक्रियण तक सीमित है। केंद्र पर सफल क्लिक से स्क्रॉलिंग, ड्रैगिंग, होवर या कॉन्टेक्स्ट मेनू का व्यवहार सिद्ध नहीं होता; यदि आपका इंटरफ़ेस इन्हें जोड़ता है, तो उनकी अलग से जाँच करें। खास तौर पर, Apple इंटरैक्शन कंटेंट-शेप को ड्रैग-प्रिव्यू और कॉन्टेक्स्ट-मेनू-प्रिव्यू शेप से अलग बताता है; वे हिट टेस्टिंग नहीं बदलते। कंटेंट-शेप पर Apple का दस्तावेज़ इन अलग प्रकारों को समझाता है।

इस डेस्कटॉप कार्य के लिए रिंग के पाथ को उसके दिखाई देने वाले बैंड और क्लिक करने योग्य क्षेत्र की साझा परिभाषा बनाए रखें। इससे पारदर्शी केंद्र का सटीक अर्थ तय होता है: ऊपरी परत उस बिंदु को अस्वीकार करती है और निचला SwiftUI कंट्रोल सामान्य इंटरैक्शन प्रणाली के माध्यम से प्रतिक्रिया देने के लिए ज़िम्मेदार रहता है।

स्रोत