I have a similar problem. My example is a bit more involved but arguably more generic. Hope it helps.
My TCA setup looks like this:
struct State: Equatable {
    var child: ChildState?
    // ...
}
One can get the child store by scoping the parent store:
let childStore = parentStore.scope { $0.child ?? ChildState() }
And the child view’s body looks like this:
var body: some View {
    WithViewStore(childStore) { viewStore in
        // ...
    }
}
Here is what happened:
- Some reducer sets parent.child = nil.
- This notifies every scoped store that the root state changes.
- Each scoped store then applies the scope function to get a new child state, and compares it with the previous one. More likely than not, we get a different child state. Some objectWillChangeis called.
- The corresponding WithViewStoreis marked dirty.
- The content closure in WithViewStoreis called, and generates a new child view tree.
- SwiftUI then generates an animation from the old child view tree to the new child view tree, in addition to the dismissal animation for the whole child view tree.
My solution is to kill step 3. First, let's extend the child state with another property:
struct ChildState: Equatable {
    var isBeingDismissed: Bool = false
}
Second, when scoping, let's do:
let childStore = parentStore.scope { x -> ChildState in
    if let child = x.child {
        return child
    }
    var child = ChildState()
    child.isBeingDismissed = true
    return true
}
Lastly, we can modify the WithViewStore with a custom removeDuplicates closure:
var body: some View {
    WithViewStore(childStore) { old, new in 
        if old == new {
            return true
        }
        // Key
        if new.isBeingDismissed {
            return true
        }
        return false
    } content: { viewStore in
        // ...
    }
}
Here is how it works. The scoped child state can be nil in two cases, when the view is first created and the view is being dismissed. In the first case, the old is nil. In the second case, the new is nil. The custom removeDuplicates essentially does this: when a child state is being dismissed, we skip any update. So the only animation being played is the transition animation. The content stays the same.