लक्ष्य-कार्रवाई को कैसे बदलें और क्लोजर के साथ प्रतिनिधि करें

आईओएस अनुप्रयोगों में डेटा और घटनाओं के प्रसंस्करण के लिए ऐप्पल विभिन्न विकल्प प्रदान करता है। UIControl घटना प्रसंस्करण लक्ष्य-कार्रवाई पैटर्न के माध्यम से होता है। UIControl के लिए प्रलेखन निम्नलिखित कहता है:
लक्ष्य-क्रिया तंत्र आपके ऐप में नियंत्रण का उपयोग करने के लिए आपके द्वारा लिखे गए कोड को सरल करता है
आइए एक बटन क्लिक करने के प्रसंस्करण का एक उदाहरण देखें:

private func setupButton() {
    let button = UIButton()
    button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
// -   
@objc private func buttonTapped(_ sender: UIButton) { }

बटन क्लिक का विन्यास और प्रसंस्करण कोड में एक दूसरे से अलग-अलग स्थित हैं। इसलिए, आपको अपने से अधिक कोड लिखना होगा। घटनाओं और नियंत्रणों की संख्या में वृद्धि के साथ समस्याएं उत्पन्न होती हैं।

UITextField टेक्स्ट को संपादित करने और मान्य करने के लिए प्रतिनिधि पैटर्न का उपयोग करता है हम इस पैटर्न के पेशेवरों और विपक्षों पर ध्यान नहीं देंगे, यहां और पढ़ें

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

क्यों जरूरी है?


प्रारंभ में, हम GitHub पर तैयार किए गए समाधानों के बारे में गए और यहां तक ​​कि कुछ समय के लिए क्लोजर का उपयोग किया लेकिन समय के साथ, हमें तीसरे पक्ष के समाधान को छोड़ना पड़ा, क्योंकि हमने वहां मेमोरी लीक की खोज की। और इसकी कुछ विशेषताएं हमें असहज लग रही थीं। फिर अपना समाधान लिखने का निर्णय लिया गया।

क्लोज़र का उपयोग करते हुए, हम इस परिणाम से काफी खुश होंगे, हम इसे लिख सकते हैं:

textField.shouldChangeCharacters { textField, range, string in
    return true
}

मूल लक्ष्य:

  • मूल प्रकार को बनाए रखते हुए, क्लोजर में, टेक्स्टफिल्ड तक पहुंच प्रदान करें। यह क्लोजर में स्रोत ऑब्जेक्ट को हेरफेर करने के लिए है, उदाहरण के लिए, टाइप कास्टिंग के बिना उस पर एक संकेतक दिखाने के लिए एक बटन पर क्लिक करके।
  • , . , .touchUpInside onTap { }, shouldChangeCharacters UITextField , .


मुख्य विचार यह है कि हमारे पास एक पर्यवेक्षक वस्तु होगी जो सभी संदेशों को बाधित करेगी और बंद होने का कारण बनेगी।

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

डिफ़ॉल्ट कार्यान्वयन के साथ एक ऑब्ज़र्वरहार्ड प्रोटोकॉल बनाएँ, ताकि इस प्रोटोकॉल के अनुरूप प्रत्येक वर्ग पर्यवेक्षक के पास पहुँच सके:

protocol ObserverHolder: AnyObject {
    var observer: Any? { get set }
}

private var observerAssociatedKey: UInt8 = 0

extension ObserverHolder {
    var observer: Any? {
        get {
            objc_getAssociatedObject(self, &observerAssociatedKey)
        }
        set {
            objc_setAssociatedObject(
                self, 
                &observerAssociatedKey, 
                newValue, 
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )
        }
    }
}

अब यह UIControl के लिए प्रोटोकॉल के अनुपालन की घोषणा करने के लिए पर्याप्त है:

extension UIControl: ObserverHolder { }

UIControl (और UITextField सहित सभी वंशज) के पास एक नई संपत्ति है जहां पर्यवेक्षक संग्रहीत किया जाएगा।

UITextFieldDelegate उदाहरण


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

final class TextFieldObserver<T: UITextField>: NSObject, UITextFieldDelegate {
    init(textField: T) {
        super.init()
        textField.delegate = self
    }
}

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

var shouldChangeCharacters: ((T, _ range: NSRange, _ replacement: String) -> Bool)?

func textField(
    _ textField: UITextField,
    shouldChangeCharactersIn range: NSRange,
    replacementString string: String
) -> Bool {
    guard 
        let textField = textField as? T, 
        let shouldChangeCharacters = shouldChangeCharacters 
    else {
        return true
    }
    return shouldChangeCharacters(textField, range, string)
}

हम क्लोजर के साथ एक नया इंटरफ़ेस लिखने के लिए तैयार हैं:

extension UITextField {
    func shouldChangeCharacters(handler: @escaping (Self, NSRange, String) -> Bool) { }
}

कुछ गलत हो गया, संकलक एक त्रुटि फेंकता है:
'स्व' केवल एक प्रोटोकॉल में या एक कक्षा में एक विधि के परिणाम के रूप में उपलब्ध है; क्या आपका मतलब था 'UITextField'
एक खाली प्रोटोकॉल हमें मदद करेगा, जिसके विस्तार में हम स्वयं को प्रतिबंधित करते हुए UITextField को एक नया इंटरफ़ेस लिखेंगे:

protocol HandlersKit { }

extension UIControl: HandlersKit { }

extension HandlersKit where Self: UITextField {
    func shouldChangeCharacters(handler: @escaping (Self, NSRange, String) -> Bool) { }
}

कोड संकलित करता है, यह एक TextFieldObserver बनाने और इसे एक प्रतिनिधि के रूप में नामित करने के लिए रहता है। इसके अलावा, यदि पर्यवेक्षक पहले से मौजूद है, तो आपको इसे अपडेट करने की आवश्यकता है ताकि अन्य क्लोजर न खोएं:

func shouldChangeCharacters(handler: @escaping (Self, NSRange, String) -> Bool) {
    if let textFieldObserver = observer as? TextFieldObserver<Self> {
        textFieldObserver.shouldChangeCharacters = handler
    } else {
        let textFieldObserver = TextFieldObserver(textField: self)
        textFieldObserver.shouldChangeCharacters = handler
        observer = textFieldObserver
    }
}

महान, अब यह कोड कार्य कर रहा है और उपयोग के लिए तैयार है, लेकिन इसे बेहतर बनाया जा सकता है। हम एक अलग विधि में TextFieldObserver बनाएंगे और अपडेट करेंगे, केवल क्लोजर का असाइनमेंट अलग होगा, जिसे हम एक ब्लॉक के रूप में पास करेंगे। एक्सटेंशन हैंडलर में मौजूदा कोड अपडेट करें:

func shouldChangeCharacters(handler: @escaping (Self, NSRange, String) -> Bool) {
    updateObserver { $0.shouldChangeCharacters = handler }
}

private func updateObserver(_ update: (TextFieldObserver<Self>) -> Void) {
    if let textFieldObserver = observer as? TextFieldObserver<Self> {
        update(textFieldObserver)
    } else {
        let textFieldObserver = TextFieldObserver(textField: self)
        update(textFieldObserver)
        observer = textFieldObserver
    }
}

अतिरिक्त सुधार


श्रृंखला विधियों की क्षमता जोड़ें। ऐसा करने के लिए, प्रत्येक विधि को स्व वापस करना होगा और @discardableResult विशेषता होनी चाहिए:

@discardableResult
public func shouldChangeCharacters(
    handler: @escaping (Self, NSRange, String) -> Bool
) -> Self

private func updateObserver(_ update: (TextFieldObserver<Self>) -> Void) -> Self {
    ...
    return self
}

एक बंद में, UITextField के लिए उपयोग हमेशा जरूरी नहीं है, और इसलिए इस तरह के स्थानों में आप लिखने के लिए `की जरूरत नहीं है कि _ हर बार in` , हम एक ही नामकरण के साथ एक विधि जोड़ने, लेकिन जरूरी स्व बिना:

@discardableResult
func shouldChangeCharacters(handler: @escaping (NSRange, String) -> Void) -> Self {
    shouldChangeCharacters { handler($1, $2) }
}

इस दृष्टिकोण के लिए धन्यवाद, आप अधिक सुविधाजनक तरीके बना सकते हैं। उदाहरण के लिए, अंतिम पाठ ज्ञात होने पर UITextField के साथ पाठ बदलना कभी-कभी अधिक सुविधाजनक होता है:

@discardableResult
public func shouldChangeString(
    handler: @escaping (_ textField: Self, _ from: String, _ to: String) -> Bool
) -> Self {
    shouldChangeCharacters { textField, range, string in
        let text = textField.text ?? ""
        let newText = NSString(string: text)
            .replacingCharacters(in: range, with: string)
        return handler(textField, text, newText)
    }
}

किया हुआ! दिखाए गए उदाहरणों में, हमने एक UITextFieldDelegate विधि को प्रतिस्थापित किया है, और शेष विधियों को बदलने के लिए, हमें TextFieldObserver के लिए नज़दीकी जोड़ने और उसी सिद्धांत द्वारा हैंडलर किट प्रोटोकॉल के विस्तार की आवश्यकता है।

बंद करने के साथ लक्ष्य-कार्रवाई की जगह


यह ध्यान देने योग्य है कि लक्ष्य-कार्रवाई के लिए एक पर्यवेक्षक को संग्रहीत करना और इस रूप में प्रतिनिधि इसे जटिल बनाता है, इसलिए हम घटनाओं के लिए UIControl में एक अन्य संबंधित वस्तु को जोड़ने की सलाह देते हैं। हम प्रत्येक घटना के लिए एक अलग ऑब्जेक्ट स्टोर करेंगे, एक शब्दकोश इस तरह के कार्य के लिए एकदम सही है:

protocol EventsObserverHolder: AnyObject {
    var eventsObserver: [UInt: Any] { get set }
}

EventObserverHolder के लिए डिफ़ॉल्ट कार्यान्वयन जोड़ना न भूलें, गेटटर में तुरंत एक खाली शब्दकोश बनाएं:

get {
    objc_getAssociatedObject(self, &observerAssociatedKey) as? [UInt: Any] ?? [:]
}

पर्यवेक्षक एक घटना के लिए एक लक्ष्य होगा:

final class EventObserver<T: UIControl>: NSObject {
    init(control: T, event: UIControl.Event, handler: @escaping (T) -> Void) {
        self.handler = handler
        super.init()
        control.addTarget(self, action: #selector(eventHandled(_:)), for: event)
    }
}

ऐसी वस्तु में, यह एक बंद को स्टोर करने के लिए पर्याप्त है। क्रिया पूर्ण होने पर, TextFieldObserver में, हम वस्तु के प्रकार को प्रस्तुत करते हैं और बंद होने का कारण बनते हैं:

private let handler: (T) -> Void

@objc private func eventHandled(_ sender: UIControl) {
    if let sender = sender as? T {
        handler(sender)
    }
}

UIControl के लिए प्रोटोकॉल अनुपालन घोषित करें:

extension UIControl: HandlersKit, EventsObserverHolder { }

यदि आपने पहले से ही प्रतिनिधियों को क्लोजर के साथ बदल दिया है, तो आपको फिर से हैंडलर किट से मिलान करने की आवश्यकता नहीं है।
यह UIControl के लिए एक नया इंटरफ़ेस लिखने के लिए बना हुआ है। नई पद्धति के अंदर, एक पर्यवेक्षक बनाएं और उसे घटनाओं में सहेजें। प्रमुख घटना का उपयोग करके शब्दकोश शब्दकोश में देखें ।rawVue:

extension HandlersKit where Self: UIControl {

    @discardableResult
    func on(_ event: UIControl.Event, handler: @escaping (Self) -> Void) -> Self {
        let observer = EventObserver(control: self, event: event, handler: handler)
        eventsObserver[event.rawValue] = observer
        return self
    }
}

आप अक्सर उपयोग की जाने वाली घटनाओं के लिए इंटरफ़ेस को पूरक कर सकते हैं:

extension HandlersKit where Self: UIButton {

    @discardableResult
    func onTap(handler: @escaping (Self) -> Void) -> Self {
        on(.touchUpInside, handler: handler)
    }
}

सारांश


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

यहाँ पूर्ण कोड: हैंडलरकिटइस रिपॉजिटरी में अधिक उदाहरण हैं: UIControl, UIBarButtonItem, UIGestureRecognizer, UITextField और UITextView।

विषय में गहन विसर्जन के लिए, मैं ईज़ी क्लोजर के बारे में लेख पढ़ने और दूसरी तरफ से समस्या के समाधान को देखने का प्रस्ताव करता हूं

हम टिप्पणियों में प्रतिक्रिया का स्वागत करते हैं। जब तक!

All Articles