Within the SwiftUI group, many individuals provide you with their very own model of a conditional view modifier. It lets you take a view, and solely apply a view modifier when the situation holds. It sometimes seems one thing like this:
extension View {
@ViewBuilder
func applyIfM: View>(situation: Bool, remodel: (Self) -> M) -> some View {
if situation {
remodel(self)
} else {
self
}
}
}
There are various weblog posts on the market with related modifiers. I feel all these weblog posts ought to include an enormous warning signal. Why is the above code problematic? Let’s take a look at a pattern.
Within the following code, we’ve a single state property myState
. When it modifications between true
and false
, we need to conditionally apply a body:
struct ContentView: View {
@State var myState = false
var physique: some View {
VStack {
Toggle("Toggle", isOn: $myState.animation())
Rectangle()
.applyIf(situation: myState, remodel: { $0.body(width: 100) })
}
}
}
Apparently, when operating this code, the animation doesn’t look easy in any respect. For those who look intently, you’ll be able to see that it fades between the “earlier than” and “after” state:
Here is the identical instance, however written with out applyIf
:
struct ContentView: View {
@State var myState = false
var physique: some View {
VStack {
Toggle("Toggle", isOn: $myState.animation())
Rectangle()
.body(width: myState ? 100 : nil)
}
}
}
And with the code above, our animation works as anticipated:
Why is the applyIf
model damaged? The reply teaches us so much about how SwiftUI works. In UIKit, views are objects, and objects have inherent identification. Which means that two objects are equal if they’re the identical object. UIKit depends on the identification of an object to animate modifications.
In SwiftUI, views are structs — worth sorts — which signifies that they do not have identification. For SwiftUI to animate modifications, it wants to check the worth of the view earlier than the animation began and the worth of the view after the animation ends. SwiftUI then interpolates between the 2 values.
To grasp the distinction in habits between the 2 examples, let’s take a look at their sorts. Here is the kind of our Rectangle().applyIf(...)
:
_ConditionalContent<ModifiedContent<Rectangle, _FrameLayout>, Rectangle>
The outermost sort is a _ConditionalContent
. That is an enum that may both include the worth from executing the if
department, or the worth from executing the else
department. When situation modifications, SwiftUI can’t interpolate between the outdated and the brand new worth, as they’ve differing kinds. In SwiftUI, when you will have an if/else
with a altering situation, a transition occurs: the view from the one department is eliminated and the view for the opposite department is inserted. By default, the transition is a fade, and that is precisely what we’re seeing within the applyIf
instance.
In distinction, that is the kind of Rectangle().body(...)
:
ModifiedContent<Rectangle, _FrameLayout>
Once we animate modifications to the body properties, there aren’t any branches for SwiftUI to contemplate. It could simply interpolate between the outdated and new worth and all the things works as anticipated.
Within the Rectangle().body(...)
instance, we made the view modifier conditional by offering a nil
worth for the width. That is one thing that just about each view modifier help. For instance, you’ll be able to add a conditional foreground shade through the use of an elective shade, you’ll be able to add conditional padding through the use of both 0 or a worth, and so forth.
Word that applyIf
(or actually, if/else
) additionally breaks your animations if you end up doing issues accurately on the “inside”.
Rectangle()
.body(width: myState ? 100 : nil)
.applyIf(situation) { $0.border(Shade.purple) }
While you animate situation
, the border is not going to animate, and neither will the body. As a result of SwiftUI considers the if/else
branches separate views, a (fade) transition will occur as an alternative.
There may be yet one more drawback past animations. While you use applyIf
with a view that incorporates a @State
property, all state will likely be misplaced when the situation modifications. The reminiscence of @State
properties is managed by SwiftUI, primarily based on the place of the view within the view tree. For instance, take into account the next view:
struct Stateful: View {
@State var enter: String = ""
var physique: some View {
TextField("My Discipline", textual content: $enter)
}
}
struct Pattern: View {
var flag: Bool
var physique: some View {
Stateful().applyIf(situation: flag) {
$0.background(Shade.purple)
}
}
}
Once we change flag
, the applyIf
department modifications, and the Stateful()
view has a brand new place (it moved to the opposite department of a _ConditionalContent
). This causes the @State
property to be reset to its preliminary worth (as a result of so far as SwiftUI is worried, a brand new view was added to the hierarchy), and the consumer’s textual content is misplaced. The identical drawback additionally occurs with @StateObject
.
The tough half about all of that is that you simply may not see any of those points when constructing your view. Your views look high quality, however perhaps your animations are a bit funky, otherwise you typically lose state. Particularly when the situation does not change all that always, you may not even discover.
I’d argue that the entire weblog posts that recommend a modifier like applyIf
ought to have a giant warning signal. The downsides of applyIf
and its variants are in no way apparent, and I’ve sadly seen a bunch of people that have simply copied this into their code bases and have been very pleased with it (till it grew to become a supply of issues weeks later). In actual fact, I’d argue that no code base ought to have this operate. It simply makes it manner too simple to unintentionally break animations or state.
For those who’re curious about understanding how SwiftUI works, you could possibly learn our guide Considering in SwiftUI, watch our SwiftUI movies on Swift Speak, or attend one in every of our workshops.