Rastreando fontes de valor para prevenir atualizações recursivas em SwiftUI
Quando envolvemos um UITextView ou NSTextView para uso em SwiftUI, nosso delegado de texto recebe cada atualização conforme o usuário digita, escrevendo isso de volta a um binding do SwiftUI. Ao fazer isso, no entanto, é acionada uma atualização do SwiftUI com o novo valor. Aplicar o valor sem condições adiciona trabalho desnecessário na formatação de texto e pode mover o ponto de inserção para o início enquanto o usuário está digitando. Para evitar isso, envoltórios comumente compararam as strings antes de atualizar a visualização de texto, mas a comparação de string pode ser muito cara.
Neste post, exploraremos como valores personalizados de transação podem rotular atualizações que originam em nossas visualizações de texto envolvidas, permitindo-nos evitar comparações caras de strings em updateUIView(_:context:) e updateNSView(_:context:).
Analisando um UIViewRepresentable típico, veremos algo como isso:
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
@Binding var text: String
func updateUIView(_ textView: UITextView, context: Context) {
guard textView.text != text else { return }
textView.text = text
}
A comparação de uma string potencialmente grande pode se tornar muito cara, mas a comparação é necessária. Sempre que o editor escreve um novo valor no binding em textViewDidChange(_:), a propriedade @State ou classe @Observable que recebe a alteração invalida seus consumidores. Nosso UIViewRepresentable ou NSViewRepresentable é um desses consumidores através de seu binding, levando à chamada do respectivo método de atualização com o valor que acabamos de escrever.
As transações do SwiftUI nos permitem registrar onde uma atualização de propriedade originou. Envolvermos uma escrita em um binding ou propriedade observável dentro de withTransaction(::_:) e anexamos um valor personalizado, como um ObjectIdentifier. Quando o SwiftUI envia a atualização de volta, o wrapper lê o valor personalizado da transação.
func textViewDidChange(_ textView: UITextView) {
withTransaction(
\.originatingTextView,
ObjectIdentifier(textView)
) {
text = textView.text
}
}
func updateUIView(_ textView: UITextView, context: Context) {
guard context.transaction.originatingTextView != ObjectIdentifier(textView) else {
return
}
textView.text = text
}
No momento de definir uma transação personalizada, assim como um valor de ambiente personalizado, podemos usar o macro @Entry do SwiftUI para declarar o valor e seu padrão diretamente em uma extensão sobre Transaction.
extension Transaction {
@Entry var originatingTextView: ObjectIdentifier? = nil
}
Este método não está limitado a visualizações de texto envoltas. É útil também para qualquer visualização envolta UIView ou NSView com um gesto que alimente até o SwiftUI. Marcar as escritas impulsionadas por gestos evita que os mesmos valores retornem na próxima iteração do ciclo de execução e sobrescrevam atualizações posteriores de gestos.

