Taking Control of SwiftUI Data View Animations Using Transactions

Published by malhal on

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:

  1. Closure-based transaction(_:): Allows in-place mutation of the active transaction associated with a binding.
  2. KeyPath-based transaction(_:_:): Provides a concise syntax for assigning a specific value to any writable keypath on Transaction.
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

Categories: SwiftUI