كيفية استبدال الإجراء الهدف وتفويض الإغلاق

توفر Apple خيارات متنوعة لمعالجة البيانات والأحداث في تطبيقات iOS. تحدث معالجة حدث 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
}

الأهداف الأساسية:

  • في الإغلاق ، وفر الوصول إلى textField ، مع الحفاظ على النوع الأصلي. هذا من أجل معالجة الكائن المصدر في عمليات الإغلاق ، على سبيل المثال ، من خلال النقر على زر لإظهار مؤشر عليه بدون نوع الصب.
  • , . , .touchUpInside onTap { }, shouldChangeCharacters UITextField , .


الفكرة الرئيسية هي أنه سيكون لدينا كائن مراقب يقوم باعتراض جميع الرسائل ويسبب الإغلاق.

أولاً ، يجب أن نقرر كيفية الاحتفاظ بالمراقب. يمنحنا Swift القدرة على الاختيار. على سبيل المثال ، يمكننا إنشاء كائن مفرد إضافي يقوم بتخزين قاموس ، حيث يكون المفتاح هو المعرف الفريد للكائنات المرصودة ، والقيمة هي المراقب نفسه. في هذه الحالة ، سيتعين علينا إدارة دورة حياة الأشياء يدويًا ، مما قد يؤدي إلى حدوث تسرب للذاكرة أو فقدان للمعلومات. يمكنك تجنب مثل هذه المشاكل إذا قمت بتخزين الكائنات ككائنات مرتبطة.

إنشاء بروتوكول ObserverHolder باستخدام افتراضي حتى يتمكن كل فئة تتوافق مع هذا البروتوكول من الوصول إلى المراقب:

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 ، مما يعني أنه يجب أن يتوافق مع بروتوكول UITextFieldDelegate. نحن بحاجة إلى نوع عام T من أجل حفظ النوع الأصلي من UITextField. مثال على هذا الشيء:

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

ستحتاج كل طريقة تفويض إلى إغلاق منفصل. داخل هذه الطرق ، سنقوم بإدخال النوع إلى T وتسبب الإغلاق المقابل. سنضيف رمز 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 وتحديثه بطريقة منفصلة ، لن يكون سوى تعيين الإغلاق مختلفًا ، والذي سنمرره في شكل كتلة. تحديث الكود الحالي في ملحق HandlersKit:

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
    }
}

تحسينات إضافية


أضف القدرة على سلسلة الطرق. للقيام بذلك ، يجب أن ترجع كل طريقة Self وأن يكون لها السمة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 وإلى تمديد بروتوكول HandlersKit بنفس المبدأ.

استبدال الإجراء الهدف بالإغلاق


تجدر الإشارة إلى أن تخزين مراقب واحد للعمل المستهدف والمفوض في هذا النموذج يعقده ، لذلك نوصي بإضافة كائن آخر مرتبط إلى UIControl للأحداث. سنقوم بتخزين كائن منفصل لكل حدث ، القاموس مثالي لمثل هذه المهمة:

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

لا تنس أن تضيف التطبيق الافتراضي لـ EventsObserverHolder ، فسننشئ قاموسًا فارغًا على الفور في لغة البدء:

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 { }

إذا كنت قد قمت بالفعل باستبدال المندوبين بإغلاق ، فلن تحتاج إلى مطابقة HandlersKit مرة أخرى.
يبقى لكتابة واجهة جديدة ل UIControl. داخل الطريقة الجديدة ، قم بإنشاء مراقب وحفظه في قاموس eventObserver باستخدام مفتاح event.rawValue:

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)
    }
}

ملخص


مرحى ، تمكنا من استبدال الإجراء المستهدف والمندوبين بالإغلاق والحصول على واجهة واحدة لعناصر التحكم. ليست هناك حاجة للتفكير في تسريبات الذاكرة والتقاط عناصر التحكم نفسها في عمليات الإغلاق ، نظرًا لأن لدينا إمكانية الوصول المباشر إليها.

الرمز الكامل هنا: HandlersKit . هناك المزيد من الأمثلة في هذا المستودع لـ: UIControl و UIBarButtonItem و UIGestureRecognizer و UITextField و UITextView.

للحصول على نظرة أعمق للموضوع ، أقترح أيضًا قراءة المقالة حول EasyClosure والنظر في حل المشكلة من الجانب الآخر.

نحن نرحب بالملاحظات في التعليقات. حتى!

All Articles