如何用闭包代替目标动作和委托

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上使用现成的解决方案,甚至使用Closures已有一段时间但是随着时间的流逝,我们不得不放弃第三方解决方案,因为我们发现那里存在内存泄漏。而且它的某些功能对我们来说似乎不舒服。然后决定编写您自己的解决方案。

当使用闭包可以编写以下代码时,我们会对结果感到满意:

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

基本目标:

  • 在闭包中,提供对textField的访问权限,同时保持原始类型。这是为了在闭包中操作源对象,例如,通过单击按钮以在其上显示指示器而无需类型转换。
  • , . , .touchUpInside onTap { }, shouldChangeCharacters UITextField , .


主要思想是我们将拥有一个观察者对象,该对象将拦截所有消息并导致关闭。

首先,我们必须决定如何保留观察者。Swift提供了选择的力量。例如,我们可以创建一个额外的单例对象,该对象将存储一个词典,其中的键是观察对象的唯一ID,其值是观察者本人。在这种情况下,我们将不得不手动管理对象的生命周期,这可能导致内存泄漏或信息丢失。如果将对象存储为关联对象,则可以避免此类问题。

创建具有默认实现的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'
空协议将对我们有帮助,在扩展中,我们将在限制Self的同时为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` ,我们添加了一个具有相同命名但没有必需的Self的方法:

@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添加默认实现,我们将立即在getter中创建一个空字典:

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的新接口。在新方法中,创建一个观察器,并使用键event.rawValue将其保存在eventsObserver字典中:

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