In the SwiftUI community, many people come up with their own version of a conditional view modifier
. It allows you to take a view, and only apply a view modifier when the condition holds. It typically looks something like this:
And with the code above, our animation works as expected:
Why is the applyIf
version broken? The answer teaches us a lot about how SwiftUI works. In UIKit, views are objects, and objects have inherent identity
. This means that two objects are equal if they are the same object. UIKit relies on the identity of an object to animate changes.
In SwiftUI, views are structs — value types — which means that they don't have identity. For SwiftUI to animate changes, it needs to compare the value of the view before
the animation started and the value of the view after
the animation ends. SwiftUI then interpolates between the two values.
To understand the difference in behavior between the two examples, let's look at their types. Here's the type of our Rectangle().applyIf(...)
The outermost type is a _ConditionalContent
. This is an enum that will either
contain the value from executing the if
the value from executing the else
branch. When condition changes, SwiftUI cannot interpolate between the old and the new value, as they have different types. In SwiftUI, when you have an if/else
with a changing condition, a transition
happens: the view from the one branch is removed and the view for the other branch is inserted. By default, the transition is a fade, and that's exactly what we are seeing in the applyIf
In contrast, this is the type of Rectangle().frame(...)
When we animate changes to the frame properties, there are no branches for SwiftUI to consider. It can just interpolate between the old and new value and everything works as expected.
In the Rectangle().frame(...)
example, we made the view modifier conditional by providing a nil
value for the width. This is something that almost every view modifier support. For example, you can add a conditional foreground color by using an optional color, you can add conditional padding by using either 0 or a value, and so on.
Note that applyIf
(or really, if/else
) also breaks your animations when you are doing things correctly on the “inside”.
When you animate condition
, the border will not animate, and neither will the frame. Because SwiftUI considers the if/else
branches separate views, a (fade) transition will happen instead.
There is yet another problem beyond animations. When you use applyIf
with a view that contains a @State
property, all state will be lost when the condition changes. The memory of @State
properties is managed by SwiftUI, based on the position of the view in the view tree. For example, consider the following view:
When we change flag
, the applyIf
branch changes, and the Stateful()
view has a new position (it moved to the other branch of a _ConditionalContent
). This causes the @State
property to be reset to its initial value (because as far as SwiftUI is concerned, a new view was added to the hierarchy), and the user's text is lost. The same problem also happens with @StateObject
The tricky part about all of this is that you might not see any of these issues when building your view. Your views look fine, but maybe your animations are a little funky, or you sometimes lose state. Especially when the condition doesn't change all that often, you might not even notice.
I would argue that all of the blog posts that suggest a modifier like applyIf
should have a big warning sign. The downsides of applyIf
and its variants are not at all obvious, and I have unfortunately seen a bunch of people who have just copied this into their code bases and were very happy with it (until it became a source of problems weeks later). In fact, I would argue that no code base should have this function
. It just makes it way too easy to accidentally break animations or state.