Skip to content

View Modifiers

Posted on:April 4, 2023

When we create views using SwiftUI, we are using a variety of built-in view modifiers — such as foregroundColor, font, or frame. View modifiers are a powerful tool in SwiftUI that allow us to configure a view or modify its behavior and appearance. By design, they’re reusable and composable. However, since a modifier is called as a function that modifies the view it’s called on, the order of the function calls matters. For example, calling background(Color.red) before padding() creates a different result than calling them in the reverse order.

Creating our own ViewModifiers

A ViewModifier is composed of two parts:

  1. A function extended on some View type that makes it accessible to callers.
  2. A ViewModifier object, which changes the behavior of the view it’s applied to.

Let’s create a custom modifier that can be applied to a View that visually makes the view take on a “warning” or “danger” appearance.

struct DangerModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .foregroundColor(.red)
            .font(.headline)
    }
}

extension View {
    func isDangerousAction() -> some View {
        self.modifier(MyCustomModifier())
    }
}

For those familiar with PointFree, Brandon Williams and Stephen Celis explored a similar construction for composable UIKit styles.

In general, when we create our own view modifiers, we should aim to follow a set of simple guidelines:

Use styles for specific Views

Several view types — like Button have a style associated with them that can encapsulate several of their properties. For example, when we are configuring a button we can create a ButtonStyle. ButtonStyle and its counterparts are quite similar to creating our own modifiers — the key difference is that we are supplied with an additional parameter that contains information about the configuration of that object.

Let’s examine a ButtonStyle for encapsulating an animation behavior:

struct PressableStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .scaleEffect(configuration.isPressed ? 0.97 : 1)
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

In the example above, we are returning the configuration’s label, which is of type some View. some View is not guaranteed to be a button. In order to make the style above composable with other styles, we need to ensure that we return a Button.

struct PressableStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    Button(configuration)
      .scaleEffect(configuration.isPressed ? 0.97 : 1)
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}