Tracking value sources to prevent recursive SwiftUI updates
When wrapping a UITextView or NSTextView for use in SwiftUI, delegate updates are usually written back to a SwiftUI binding. SwiftUI then calls updateUIView(_:context:) or updateNSView(_:context:) with the same value on the next run loop iteration. Applying unchanged text can perform unnecessary layout work and move the insertion point while the user is typing, so a typical UIViewRepresentable compares the strings before updating the text view.
// UITextViewDelegate callback
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
// UIViewRepresentable property
@Binding var text: String
func updateUIView(_ textView: UITextView, context: Context) {
// Comparing large strings on every update can be expensive.
guard textView.text != text else { return }
textView.text = text
}
For large text values, we can avoid the string comparison by using a custom SwiftUI transaction value to record where the update originated. First, define an optional ObjectIdentifier on Transaction using the @Entry macro.
extension Transaction {
@Entry var originatingTextView: ObjectIdentifier? = nil
}
In textViewDidChange(_:), wrap the binding write in withTransaction(::_:) and store the identity of the text view that produced the update.
func textViewDidChange(_ textView: UITextView) {
withTransaction(
\.originatingTextView,
ObjectIdentifier(textView)
) {
text = textView.text
}
}
The representable can then compare the transaction value with the identity of the text view being updated.
func updateUIView(_ textView: UITextView, context: Context) {
// Comparing fixed-size identity values is inexpensive.
guard context.transaction.originatingTextView != ObjectIdentifier(textView) else {
return
}
textView.text = text
}
Updates made elsewhere that omit the custom withTransaction call use the default nil value and continue to update the wrapped view normally.



