Taking Control of SwiftUI Data View Animations Using Transactions
If you’ve ever built a data view in SwiftUI using List, Table or VStack, you’ve probably noticed an annoying quirk: whenever the data re-sorts, SwiftUI treats it as a layout change and animates all the rows flying around. Even worse, it often triggers an unwanted scale/fade animation when the view first appears.
To work around this, we usually want animations enabled for structural changes (like inserting, deleting, or moving items), but completely disabled for sorting updates.
The Solution: Leveraging .disablesAnimations on Transactions
While exploring custom TransactionKey flags to manage this, I realized that SwiftUI’s built-in view modifiers like .animation(_:value:) evaluate transactions directly. Specifically, when transaction.disablesAnimations is set to true, view-level animation modifiers gracefully back off.
To make this clean and ergonomic at the binding level, I’ve submitted a pull request to BindingKit adding a pair of transaction-transforming extensions to Binding. This means we no longer need a custom isSorting transaction key at all—we can just flip the native flag inline.
How It Looks
Here is how clean your view code becomes using the new transaction extension from BindingKit:
Table(items, sortOrder: $sortDescriptors.transaction(\.disablesAnimations, true)) {
TableColumn("Timestamp", value: \.timestamp) { item in
ItemRow(item: item)
}
}
.animation(.default, value: items.map(\.id))
What’s in the Pull Request (BindingKit)
The PR introduces two flexible overloads to Binding:
- Closure-based
transaction(_:): Allows in-place mutation of the active transaction associated with a binding. - KeyPath-based
transaction(_:_:): Provides a concise syntax for assigning a specific value to any writable keypath onTransaction.
extension Binding {
public func transaction(_ transform: @escaping (inout Transaction) -> Void) -> Self {
var tx = self.transaction
transform(&tx)
return self.transaction(tx)
}
public func transaction<V>(_ keyPath: WritableKeyPath<Transaction, V>, _ value: V) -> Self {
transaction { transaction in
transaction[keyPath: keyPath] = value
}
}
}
Check out Pull Request #2 on GitHub