स्विफ्टयूआई द्वारा ड्रैग का पूर्वावलोकन करते समय आकृति की प्रतिबद्ध स्थिति को अपरिवर्तित रखें, फिर onEnded में अंतिम अनुवाद को प्रतिबद्ध करें। यह ऐपकिट माउस-अप टाइमिंग पर भरोसा किए बिना प्रारंभिक स्थिति को पूरा होने तक संरक्षित करता है। यहां आपके अपने मैक ऐप में एक आयत को ले जाने का एक पूर्ण उदाहरण है।

कौन सी पूर्णता सीमा प्रारंभिक स्थिति की मालिक होनी चाहिए?

आकृति-स्थानांतरण इंटरैक्शन के लिए, ठोस कार्य प्रत्येक पूर्वावलोकन और अंतिम स्थिति की गणना एक ही प्रारंभिक स्थिति से करना है। उपयोगी पूर्णता सीमा उस आकृति के जेस्चर का सफल समापन है। ऐप्पल दस्तावेज़ करता है कि onEnded(_:) जेस्चर का अंतिम मान प्रदान करता है और केवल तभी चलता है जब जेस्चर सफलतापूर्वक समाप्त होता है। यह @GestureState की सिफारिश करता है उस अस्थायी स्थिति के लिए जिसे जेस्चर कैसे भी समाप्त हो, रीसेट करना आवश्यक है। ऐप्पल का onEnded(_:) दस्तावेज़ीकरण।

इंटरैक्शन को दो प्रकार की स्थिति दें: एक प्रतिबद्ध मॉडल स्थिति और एक अस्थायी पूर्वावलोकन अनुवाद। ड्रैगिंग के दौरान, उनका योग प्रदर्शित करें। सफल समापन पर, अंतिम अनुवाद को प्रतिबद्ध स्थिति पर एक बार लागू करें। यह ऐप्पल के क्षणिक जेस्चर अपडेट और स्थायी स्थिति अपडेट के बीच अंतर से प्राप्त एक डिज़ाइन है। जेस्चर के साथ इंटरएक्टिविटी जोड़ना।

जानकारी इस उदाहरण में मालिक जीवनकाल
आयत का प्रतिबद्ध केंद्र निजी @State मॉडल जेस्चर पूरा होने के बाद भी बना रहता है
वर्तमान पूर्वावलोकन अनुवाद @GestureState जब जेस्चर निष्क्रिय हो जाता है तो रीसेट हो जाता है
अंतिम अनुवाद onEnded कॉलबैक तर्क चाल को प्रतिबद्ध करने के लिए तुल्यकालिक रूप से उपयोग किया जाता है
ऐपकिट माउस-अप इवेंट ऐपकिट इवेंट हैंडलिंग बटन रिलीज़ की रिपोर्ट करता है; स्विफ्टयूआई जेस्चर पूर्णता को परिभाषित नहीं करता

ऐप्पल @State को स्विफ्टयूआई-प्रबंधित स्टोरेज और व्यू पदानुक्रम में मूल्यों के लिए सत्य के स्रोत के रूप में दस्तावेज़ करता है। @GestureState स्वचालित रूप से रीसेट हो जाता है जब उसका जेस्चर निष्क्रिय हो जाता है। बेसलाइन को पूर्व में और पूर्वावलोकन को बाद में रखने से प्रत्येक मान को इस इंटरैक्शन के लिए आवश्यक जीवनकाल मिलता है। State, GestureState।

एक पूर्ण मैक आयत-ड्रैग उदाहरण बनाएं

Xcode में macOS 14 या बाद के डिप्लॉयमेंट टार्गेट के साथ एक macOS स्विफ्टयूआई ऐप बनाएं। जनरेट किए गए ऐप डिक्लेरेशन और कंटेंट व्यू को निम्नलिखित सिंगल फाइल से बदलें, यह सुनिश्चित करते हुए कि टार्गेट में केवल एक @main डिक्लेरेशन हो। उदाहरण पूरी तरह से मेमोरी में संग्रहीत एक सिंथेटिक आयत का उपयोग करता है। इसके नामित-निर्देशांक-स्थान एपीआई macOS 14 से उपलब्ध होने के रूप में दस्तावेज़ीकृत हैं। ड्रैग जेस्चर इनिशियलाइज़र, नामित निर्देशांक स्थान।

import SwiftUI

@main
struct GestureStartDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeCanvas()
        }
    }
}

private struct SampleItem {
    var center = CGPoint(x: 160, y: 140)
}

private struct ShapeCanvas: View {
    @State private var item = SampleItem()
    @GestureState private var previewTranslation = CGSize.zero

    private let canvasWidth: CGFloat = 600
    private let canvasHeight: CGFloat = 360
    private let shapeWidth: CGFloat = 80
    private let shapeHeight: CGFloat = 60

    private func projectedCenter(_ translation: CGSize) -> CGPoint {
        let proposedX = item.center.x + translation.width
        let proposedY = item.center.y + translation.height
        let halfWidth = shapeWidth / 2
        let halfHeight = shapeHeight / 2

        return CGPoint(
            x: min(max(proposedX, halfWidth),
                   canvasWidth - halfWidth),
            y: min(max(proposedY, halfHeight),
                   canvasHeight - halfHeight)
        )
    }

    private var moveGesture: some Gesture {
        DragGesture(
            minimumDistance: 3,
            coordinateSpace: .named("shapeCanvas")
        )
        .updating($previewTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            let finalCenter = projectedCenter(value.translation)
            item.center = finalCenter
        }
    }

    var body: some View {
        let displayedCenter = projectedCenter(previewTranslation)

        VStack(alignment: .leading, spacing: 12) {
            Text("Drag the blue rectangle")
                .font(.headline)

            ZStack {
                Color.gray.opacity(0.12)

                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.blue)
                    .frame(width: shapeWidth, height: shapeHeight)
                    .contentShape(Rectangle())
                    .gesture(moveGesture)
                    .position(
                        x: displayedCenter.x,
                        y: displayedCenter.y
                    )
            }
            .frame(width: canvasWidth, height: canvasHeight)
            .coordinateSpace(.named("shapeCanvas"))
            .clipped()

            Text(
                "Committed center: " +
                "\(Int(item.center.x)), \(Int(item.center.y))"
            )
            .monospacedDigit()
        }
        .padding(20)
    }
}

मुख्य अपरिवर्तनीय यह है कि item.center updating में कभी नहीं बदलता। यह onEnded द्वारा finalCenter की गणना करने तक जेस्चर का मॉडल बेसलाइन बना रहता है। अंतिम गणना value.translation को सीधे पढ़ती है, इसलिए इसे पूरा होने के दौरान पूर्वावलोकन संपत्ति को अपना अंतिम मान बनाए रखने की आवश्यकता नहीं होती।

जेस्चर एनक्लोजिंग कैनवास के नामित निर्देशांक स्थान में मूवमेंट को मापता है। आयत का position(x:y:) उसके केंद्र को उसी पैरेंट स्थान में रखता है। उन स्थानों का मिलान एक चलती आकृति के स्थानीय निर्देशांकों को कैनवास निर्देशांकों के साथ मिलाने से बचाता है। ऐप्पल नामित-स्थान जेस्चर माप और केंद्र-आधारित पोजिशनिंग दोनों को दस्तावेज़ करता है। coordinateSpace(_:), position(x:y:)।

साझा projectedCenter फ़ंक्शन आयत को कैनवास के अंदर भी रखता है। पूर्वावलोकन और प्रतिबद्धता समान बाउंड्स का उपयोग करते हैं: क्षैतिज केंद्र 40 से 560 तक, और लंबवत केंद्र 30 से 330 तक। एक किनारे पर, मूवमेंट मूल ड्रैग प्रारंभ से कुल विस्थापन पर आधारित रहता है; आयत किनारे से दूर चला जाता है एक बार वह प्रस्तावित स्थिति बाउंड्स के अंदर वापस आ जाती है।

कॉन्फ़िगर किए गए 3-पॉइंट पहचान थ्रेशोल्ड से छोटा मूवमेंट एक पहचाने गए ड्रैग का गठन नहीं करता। यह उदाहरण जानबूझकर प्रारंभ को एक पहचाने गए आकृति-स्थानांतरण इंटरैक्शन की शुरुआत के रूप में मानता है। भौतिक माउस-डाउन इवेंट पर मॉडल स्थिति को कैप्चर करने की आवश्यकता एक अलग सीमा है। ड्रैग जेस्चर इनिशियलाइज़र।

प्रारंभिक स्थिति अंतिम अपडेट से क्यों बची रहती है

ऐप्पल DragGesture.Value.translation को जेस्चर की शुरुआत से उसके वर्तमान इवेंट तक कुल अनुवाद के रूप में परिभाषित करता है। यह संचयी है, न कि पिछले कॉलबैक के बाद का डेल्टा। नतीजतन, प्रत्येक रिपोर्ट किए गए अनुवाद को पहले से अपडेट की गई मॉडल स्थिति में जोड़ने से मूवमेंट की ओवरकाउंटिंग होगी। DragGesture.Value।

इन स्पष्ट रूप से दृष्टांत इनपुट पर विचार करें, सभी कैनवास बाउंड्स के भीतर:

चरण रिपोर्ट किया गया कुल अनुवाद प्रतिबद्ध केंद्र प्रदर्शित केंद्र
ड्रैगिंग से पहले (0, 0) (160, 140) (160, 140)
पहला अपडेट (12, 5) (160, 140) (172, 145)
दूसरा अपडेट (30, -10) (160, 140) (190, 130)
सफल अंत (45, 20) बन जाता है (205, 160) स्थिर होता है (205, 160)

हर पूर्वावलोकन एक ही बेसलाइन का उपयोग करता है। पूर्णता अंतिम (45, 20) को एक बार लागू करती है। अनुवाद (-15, 10) के साथ एक बाद का सफल ड्रैग तब (205, 160) से शुरू होता है और (190, 170) को प्रतिबद्ध करता है। इन गणनाओं को एक नियतात्मक पायथन गणना से जांचा गया था। स्विफ्टयूआई एप्लिकेशन का निरीक्षण पुनर्प्राप्त API दस्तावेज़ीकरण के विरुद्ध किया गया था लेकिन इस वातावरण में संकलित या चलाया नहीं गया था।

आयत के प्रारंभिक केंद्र के लिए startLocation को प्रतिस्थापित न करें। ऐप्पल startLocation को ड्रैग के पहले इवेंट के स्थान के रूप में परिभाषित करता है। एक पॉइंटर आयत के कोने के पास शुरू हो सकता है, इसलिए वह स्थान और आकृति का मॉडल केंद्र भिन्न हो सकते हैं। इसी तरह, predictedEndTranslation एक वेग-आधारित भविष्यवाणी है; इस डायरेक्ट-प्लेसमेंट इंटरैक्शन के लिए अंतिम translation का उपयोग करें। DragGesture.Value।

इस डिज़ाइन में रद्दीकरण के लिए प्रतिबद्ध स्थिति के रोलबैक की आवश्यकता नहीं है क्योंकि पूर्वावलोकन अपडेट कभी इसे ओवरराइट नहीं करते। ऐप्पल दस्तावेज़ करता है कि जेस्चर स्थिति तब रीसेट होती है जब जेस्चर निष्क्रिय हो जाता है, जबकि onEnded केवल सफलता के बाद चलता है। इसलिए अस्थायी विस्थापन एक रद्द चाल को प्रतिबद्ध किए बिना गायब हो जाता है। यह निष्कर्ष उन दस्तावेज़ीकृत व्यवहारों से निकलता है; यह रीसेट और सफल-पूर्णता कॉलबैक के बीच एक क्रम मानने पर निर्भर नहीं करता। GestureState, onEnded(_:)।

ऐपकिट माउस-अप मॉनिटर को बेसलाइन क्यों नहीं साफ़ करनी चाहिए

एक ऐपकिट लोकल इवेंट मॉनिटर ऐप को डिस्पैच से पहले इवेंट प्राप्त करता है। इसलिए एक मॉनिटर किए गए माउस-अप इवेंट से साझा जेस्चर-प्रारंभ स्थिति को साफ़ करना उसे डाउनस्ट्रीम हैंडलिंग द्वारा उस इवेंट को प्रोसेस करने से पहले हटा सकता है। यह प्रलेखित डिस्पैच सीमा का परिणाम है, न कि प्रलेखित स्विफ्टयूआई कॉलबैक-क्रम गारंटी। addLocalMonitorForEvents(matching:handler:)।

इसी तरह, NSResponder.mouseUp(with:) रिपोर्ट करता है कि उपयोगकर्ता ने बायां माउस बटन जारी किया। इसका अनुबंध यह घोषित नहीं करता कि हर स्विफ्टयूआई जेस्चर कॉलबैक या व्यू अपडेट पूरा हो चुका है। कच्चे-इवेंट अवलोकन और पहचाने गए-जेस्चर पूर्णता को अलग-अलग जिम्मेदारियों के रूप में रखें। mouseUp(with:)।

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

यदि इंटरैक्शन को एक कस्टम ऐपकिट व्यू में लागू किया जाना चाहिए, तो उस व्यू को अपना इनपुट अनुक्रम और सत्र स्थिति स्वयं रखने दें, और इसे NSViewRepresentable के माध्यम से एकीकृत करें। ऐप्पल उस एकीकरण के लिए निर्माण, अपडेट, कोऑर्डिनेटर, और डिसमेंटलिंग हुक प्रदान करता है। प्रतिनिधित्व किए गए व्यू के सत्र को ऐपकिट व्यू या उसके कोऑर्डिनेटर में रखें, बजाय प्रत्येक प्रतिनिधित्व योग्य मान से इसे पुनर्निर्माण करने के। स्विफ्टयूआई प्रतिनिधित्व किए गए व्यू के लेआउट को नियंत्रित करता है, इसलिए स्वतंत्र रूप से उसके प्रबंधित फ्रेम या बाउंड्स को न बदलें। NSViewRepresentable।

इंटरैक्शन को कब एक स्पष्ट स्नैपशॉट की आवश्यकता होती है?

उदाहरण की प्रतिबद्ध स्थिति पर्याप्त है क्योंकि ड्रैग सक्रिय होने पर कोई अन्य क्रिया इसे नहीं बदलती। एक बड़े कैनवास के लिए, एक अलग सत्र ऑब्जेक्ट पेश करने से पहले इस निर्णय सहायता का उपयोग करें:

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

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

यदि आप एक स्थायी सत्र पेश करते हैं, तो सफल पूर्णता, रद्दीकरण, और व्यू हटाने को स्पष्ट रूप से परिभाषित करें। इसे केवल onEnded में साफ़ करने से रद्दीकरण अनसुलझा रह जाता है, क्योंकि वह कॉलबैक केवल-सफलता है। जेस्चर-स्थिति रीसेट ट्रांजैक्शन को एक सार्वभौमिक पूर्णता अधिसूचना के रूप में पुन: उपयोग न करें: ऐप्पल अपने रीसेट क्लोजर को जेस्चर स्थिति रीसेट करने के लिए एक ट्रांजैक्शन प्रदान करने के रूप में दस्तावेज़ करता है। onEnded(_:), init(wrappedValue:reset:)।

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

स्रोत