Conditional View Modifier creation in SwiftUI allows you only to apply modifiers if a certain condition is true. Whether it’s a simple checkbox value or an OS availability check, there are many cases in which you want to apply different configurations to your views.
A View Modifier in SwiftUI modifies the View with given configurations. An example is applying a background color to a View. A common use case I often run into in which I want to apply different modifiers is applying specific fixes for specific OS versions. For example: “Apply this modifier for iOS 13.1 to fix a little bug”.
Unfortunately, there’s no easy way to apply conditional modifiers in SwiftUI, so let’s create a useful extension for this.
Creating a Conditional View Modifier extension
Let’s assume we have a scenario where we want to have a background color set for iOS 13 only. Without having a custom extension, this could look as follows:
struct ContentView: View {
var body: some View {
if #available(iOS 14.0, *) {
Text("Hello, world!")
.padding()
} else {
/// iOS 13 only
Text("Hello, world!")
.background(Color.red) // Apply a red color
.padding()
}
}
}
As you can see, we have copied most of our view defining logic. The only difference is applying the red color in the case of iOS 13. If we were to do this in multiple places, it becomes a huge challenge to maintain our views.
By creating a custom extension on View
we allow ourselves to write this in a more efficient manner:
extension View {
/// Applies the given transform if the given condition evaluates to `true`.
/// - Parameters:
/// - condition: The condition to evaluate.
/// - transform: The transform to apply to the source `View`.
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
This extension gives us a new if
method to use on any view. We can rewrite the example we showed before as follows:
struct ContentView: View {
private var shouldApplyBackground: Bool {
guard #available(iOS 14, *) else {
return true
}
return false
}
var body: some View {
Text("Hello, world!")
.padding()
.if(shouldApplyBackground) { view in
// We only apply this background color if shouldApplyBackground is true
view.background(Color.red)
}
}
}
This is great! The View Modifier is conditionally applied, and we only have to maintain our main View logic once. However, it does require us to create computed properties for each conditional View Modifier, which is not ideal. It works great if you have a boolean-based modifier, but it works less for OS-specific checks.
There’s honestly not a great built-in feature to solve this as the availability APIs require us to use either a guard, if, or while statement. Let’s explore a few options we could use.
Using @autoclosure for inline statements
By modifying our if
extension method using @autoclosure we allow ourselves to write an inline statement.
extension View {
/// Applies the given transform if the given condition evaluates to `true`.
/// - Parameters:
/// - condition: The condition to evaluate.
/// - transform: The transform to apply to the source `View`.
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
if condition() {
transform(self)
} else {
self
}
}
}
The @autoclosure keyword allows us to either use a boolean statement directly like in our previous code examples or to pass in a closure manually to create a conditional view modifier:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.if({
if #available(iOS 14, *) {
return true
}
return false
}()) { view in
view.background(Color.red)
}
}
}
By allowing ourselves to make inline statements, we take away the requirement of creating computed properties. However, the code becomes less readable and is less scalable if applied in multiple places through your code.
Creating a Bool extension for OS specific checks
The great thing about Swift is that we can extend almost every type, including booleans. Therefore, we can create the following extension to check for iOS 13:
extension Bool {
static var iOS13: Bool {
guard #available(iOS 14, *) else {
// It's iOS 13 so return true.
return true
}
// It's iOS 14 so return false.
return false
}
}
We can now revert our @autoclosure change from before and use the boolean extension directly:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.if(.iOS13) { view in
view.background(Color.red)
}
}
}
This is great! We got ourselves a readable view combined with a conditional View Modifier based on a boolean that checks for iOS 13 only. The only downside is that we have to create a boolean extension for each OS check we want to do, but we only need a few in practice in my experience.
Conclusion
Using a Conditional View Modifier allows us to change our view based on certain conditions. If a given condition evaluates to true, we apply the given modifier to change our views’ configuration. OS-specific checks complicate things a little, but we solve this quite nicely with an extension on the Bool type.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!