Como substituir a ação-alvo e delegar por fechamentos

A Apple fornece várias opções para o processamento de dados e eventos em aplicativos iOS. O processamento de eventos UIControl ocorre através do padrão de ação de destino. A documentação do UIControl diz o seguinte:
O mecanismo de ação-alvo simplifica o código que você escreve para usar os controles em seu aplicativo
Vejamos um exemplo de processamento de um clique no botão:

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

A configuração e o processamento do clique do botão estão localizados separadamente um do outro no código. Portanto, você precisa escrever mais código do que gostaria. Os problemas surgem com um aumento no número de eventos e controles.

UITextField usa o padrão de delegação para editar e validar o texto . Não vamos nos concentrar nos prós e contras desse padrão, leia mais aqui .

Vários métodos de processamento de dados em um projeto geralmente levam ao fato de que o código se torna mais difícil de ler e entender. Este artigo descobrirá como trazer tudo para um único estilo usando a conveniente sintaxe de fechamento.

Por que isso é necessário?


Inicialmente, desenvolvemos soluções prontas no GitHub e até usamos Closures por algum tempo . Mas, com o tempo, tivemos que abandonar a solução de terceiros, porque descobrimos vazamentos de memória lá. E algumas de suas características pareciam desconfortáveis ​​para nós. Decidiu-se então escrever sua própria solução.

Ficaríamos muito felizes com o resultado quando, usando encerramentos, poderíamos escrever o seguinte:

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

Objetivos básicos:

  • No fechamento, forneça acesso ao textField, mantendo o tipo original. Isso é para manipular o objeto de origem nos fechamentos, por exemplo, clicando em um botão para mostrar um indicador nele sem conversão de tipo.
  • , . , .touchUpInside onTap { }, shouldChangeCharacters UITextField , .


A idéia principal é que teremos um objeto observador que interceptará todas as mensagens e causará fechamento.

Primeiro, devemos decidir como manter o observador. Swift nos dá o poder de escolher. Por exemplo, podemos criar um objeto singleton adicional que armazenará um dicionário, onde a chave é o ID exclusivo dos objetos observados e o valor é o próprio observador. Nesse caso, teremos que gerenciar o ciclo de vida dos objetos manualmente, o que pode levar a vazamentos de memória ou perda de informações. Você pode evitar esses problemas se armazenar objetos como objetos associados.

Crie um protocolo ObserverHolder com uma implementação padrão para que cada classe que esteja em conformidade com este protocolo tenha acesso ao observador:

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

Agora basta declarar a conformidade com o protocolo do UIControl:

extension UIControl: ObserverHolder { }

O UIControl (e todos os descendentes, incluindo o UITextField) possui uma nova propriedade em que o observador será armazenado.

Exemplo de UITextFieldDelegate


O observador será o representante do UITextField, o que significa que ele deve estar em conformidade com o protocolo UITextFieldDelegate. Precisamos de um tipo genérico T para salvar o tipo original de UITextField. Um exemplo desse objeto:

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

Cada método de delegado precisará de um fechamento separado. Dentro desses métodos, converteremos o tipo em T e causaremos o fechamento correspondente. Adicionaremos o código TextFieldObserver e, por exemplo, adicionaremos apenas um método:

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

Estamos prontos para escrever uma nova interface com fechamentos:

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

Algo deu errado, o compilador lança um erro:
'Self' está disponível apenas em um protocolo ou como resultado de um método em uma classe; você quis dizer 'UITextField'
Um protocolo vazio nos ajudará, na extensão da qual escreveremos uma nova interface para o UITextField, enquanto restringiremos o Self:

protocol HandlersKit { }

extension UIControl: HandlersKit { }

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

O código é compilado, resta criar um TextFieldObserver e designá-lo como um delegado. Além disso, se o observador já existir, você precisará atualizá-lo para não perder outros fechamentos:

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

Ótimo, agora esse código está funcionando e pronto para uso, mas pode ser aprimorado. Criaremos e atualizaremos o TextFieldObserver em um método separado, apenas a atribuição do fechamento será diferente, que passaremos na forma de um bloco. Atualize o código existente na extensão 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
    }
}

Melhorias adicionais


Adicione a capacidade de encadear métodos. Para fazer isso, cada método deve retornar Self e ter o atributo @discardableResult:

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

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

Em um fechamento, o acesso ao UITextField nem sempre é necessário e, nesses locais, você não precisa escrever ` _ in` a cada vez , adicionamos um método com a mesma nomeação, mas sem o Self necessário:

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

Graças a essa abordagem, você pode criar métodos mais convenientes. Por exemplo, alterar o texto com UITextField às vezes é mais conveniente quando o texto final é conhecido:

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

Feito! Nos exemplos mostrados, substituímos um método UITextFieldDelegate e, para substituir os métodos restantes, precisamos adicionar fechamentos ao TextFieldObserver e à extensão do protocolo HandlersKit pelo mesmo princípio.

Substituindo ação-alvo por fechamento


É importante notar que o armazenamento de um observador para a ação-alvo e o delegado neste formulário o complica, por isso recomendamos adicionar outro objeto associado ao UIControl para eventos. Vamos armazenar um objeto separado para cada evento, um dicionário é perfeito para essa tarefa:

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

Não se esqueça de adicionar a implementação padrão para EventsObserverHolder, crie um dicionário vazio imediatamente no getter:

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

O observador será o alvo de um evento:

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

Nesse objeto, é suficiente armazenar um fechamento. Após a conclusão da ação, como no TextFieldObserver, apresentamos o tipo do objeto e causamos um fechamento:

private let handler: (T) -> Void

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

Declarar conformidade do protocolo para o UIControl:

extension UIControl: HandlersKit, EventsObserverHolder { }

Se você já substituiu os delegados por encerramentos, não precisará corresponder ao HandlersKit novamente.
Resta escrever uma nova interface para o UIControl. Dentro do novo método, crie um observador e salve-o no dicionário eventsObserver usando a chave 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
    }
}

Você pode complementar a interface para eventos usados ​​com freqüência:

extension HandlersKit where Self: UIButton {

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

Sumário


Hurrah, conseguimos substituir a ação-alvo e os delegados por fechamentos e obtivemos uma única interface para controles. Não há necessidade de pensar em vazamentos de memória e capturar os próprios controles nos fechamentos, pois temos acesso direto a eles.

Código completo aqui: HandlersKit . Existem mais exemplos neste repositório para: UIControl, UIBarButtonItem, UIGestureRecognizer, UITextField e UITextView.

Para uma visão mais aprofundada do tópico, também proponho ler o artigo sobre o EasyClosure e analisar a solução para o problema do outro lado.

Agradecemos o feedback nos comentários. Até!

All Articles