Date:

Share:

Why Conditional View Modifiers are a Bad Idea · objc.io

Related Articles

In the SwiftUI community, many people come up with their own version of a Changes conditional view
. This allows you to take a photo of a view and apply a view change only when the condition is met. It usually looks something like this:

								
extension View 
    @ViewBuilder
    func applyIfM: View>(condition: Bool, transform: (Self) -> M) -> some View 
        if condition 
            transform(self)
         else 
            self
        
    


							

There are many blog posts with similar facilities. I think all of these blog posts should come with a huge warning sign. Why is the above code problematic? Let’s look at an example.

In the following code, we have a single country property myState
. When it varies between true
and false
, We want to apply a conditional framework:

								struct ContentView: View 
    @State var myState = false
    var body: some View 
        VStack 
            Toggle("Toggle", isOn: $myState.animation())
            Rectangle()
                .applyIf(condition: myState, transform:  $0.frame(width: 100) )
        
        
    


							

Interestingly, when you run this code, the animation does not look smooth at all. If you look closely, you can see that it fades between “before” and “after”:

Here is the same example, but written without applyIf
:

								struct ContentView: View 
    @State var myState = false
    var body: some View 
        VStack 
            Toggle("Toggle", isOn: $myState.animation())
            Rectangle()
                .frame(width: myState ? 100 : nil)
        
        
    


							

And with the code above, our animation works as expected:

Why the applyIf
Broken version? The answer teaches us a lot about how SwiftUI works. In UIKit, views are objects, and objects have objects identity
. This means that two objects are equal if they are the same object. UIKit relies on object identity to animate changes.

In SwiftUI, views are structures – types of values ​​- which means they have no identity. For SwiftUI to animate changes, it needs to compare the value of the display before
The animation started and the value of the display after the
The animation ends. Then, SwiftUI combines the two values.

To understand the difference in behavior between the two examples, let’s look at their types. Here’s our type Rectangle().applyIf(...)
:

								_ConditionalContent<ModifiedContent<Rectangle, _FrameLayout>, Rectangle>

							

The most external type is a _ConditionalContent
. This is a license to do so or
Contain the value from the execution of the if
branch, or
The value from the execution of the else
branch. When the situation changes, SwiftUI can not interpolate between the old and new value, because they have different types. In SwiftUI, when you have if/else
With a changing condition, a transition
Happens: The view from one branch is removed and the display of the other branch is inserted. By default, the transition is decay, which is exactly what we see in applyIf
Example.

On the other hand, it’s kind of Rectangle().frame(...)
:

								ModifiedContent<Rectangle, _FrameLayout>

							

When we animate changes to frame properties, there are no SwiftUI branches to consider. It can simply combine the old value with the new and everything works as expected.

In the Rectangle().frame(...)
For example, we have made the change of view conditional by providing a nil
Value for width. This is something that almost every display view supports. For example, you can add conditional forward color using an optional color, you can add conditional padding using 0 or a value and so on.

It noted applyIf
(Or really, if/else
) Also breaks your animations when you do things right in “Inside”.

								Rectangle()
    .frame(width: myState ? 100 : nil)
    .applyIf(condition)  $0.border(Color.red) 

							

When you are animated condition
, The border will not live, nor will the framework. Because SwiftUI considers if/else
Branches separate views, instead it will happen transition (decay).

There is another problem beyond animations. When you use applyIf
With a landscape containing a @State
Property, the whole country will lose when the situation changes. The memory of @State
The properties are managed by SwiftUI, based on the display location in the display tree. For example, consider the following view:

								struct Stateful: View 
    @State var input: String = ""
    var body: some View 
        TextField("My Field", text: $input)
    


struct Sample: View 
    var flag: Bool
    var body: some View 
        Stateful().applyIf(condition: flag) 
            $0.background(Color.red)
        
    


							

When we change flag
, God applyIf
Branch changes, and Stateful()
View has a new location (it has moved to the second branch of a _ConditionalContent
). It causes @State
A property has to be reset to its initial value (because for SwiftUI, a new view has been added to the hierarchy), and the user’s text is lost. The same problem happens with @StateObject
.

The tricky part of it all is that you may not see any of these issues when building your display. Your views look fine, but maybe your animations are a bit funky, or you sometimes lose track. Especially when the situation does not change so much, you may not even notice.

I would argue that all the blog posts that offer sub-love applyIf
There should be a large warning sign. The disadvantages of applyIf
And its versions are not clear at all, and unfortunately I saw a bunch of people who had just copied it to their code bases and were very happy with it (until it became a source of problems weeks later). In fact, I would argue so No code base should have this function
. It just makes it too easy to accidentally break animations or mode.

If you are interested in understanding how SwiftUI works, you can read our book Thinking in SwiftUI, Watch SwiftUI videos
In Swift Talk, or attend one of our workshops.

Source

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Popular Articles