SwiftUI View Modifiers Tutorial for iOS

Learn how to refactor your code to create powerful custom SwiftUI view modifiers. Make your views look consistent and your code easier to read and maintain. By Danijela Vrzan.

4.7 (3) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Extending the Text View

Open ViewModifiers.swift and replace // TODO: 2 with:

extension Text {
  func detailedInfoTitle() -> some View {
    modifier(DetailedInfoTitleModifier())
  }
}

Here, you create a Text extension with a detailedInfoTitle() method that returns
some View. Inside, you add one line of code that calls the modifier you’ve created in a previous step.

The name of the method is the name of your custom modifier. Now you don’t have to call .modifier() every time you apply it.

Open PetDetailedInformationView.swift and replace .modifier(DetailedInfoTitleModifier()) with a call to this new method:

.detailedInfoTitle()

Replace it under all five titles.

Build and run. Again, there’ll be no visible changes to your UI:

Pet detailed information

Congratulations! You’ve built your first view modifier.

Why did you make me go through all this to move my code to another file, you ask?

Well, the code you have in front of you is a simple app with a simple UI. When you created the modifier and moved a few lines of code into a separate file, you made your view smaller and more readable. This becomes noticeable on larger projects that have thousands of lines of code.

But you’re not done yet!

Next, you’ll learn how to create a button modifier with custom styling that changes depending on its isPressed property.

Building a Button Style

The most common use case for creating custom view modifiers is reusing the same code in multiple places. Here, you’ll do just that by creating a button and giving it a unique style that you’ll reuse across the app.

Open AdoptionFormView.swift. You’ll see a button with Text("Adopt Me") and a set of styling modifiers on the button’s label:

Set of modifiers applied on button's label

Now it’s time to learn how to create a custom button style modifier and apply it to the button component.

Creating a Button ViewModifier

Open ViewModifiers.swift and replace // TODO: 3 with:

struct ButtonLabelModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(.title2)
      .padding(.horizontal, 30)
      .padding(.vertical, 8)
      .foregroundColor(Color.pink)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color.pink, lineWidth: 1.5)
      )
  }
}

Here, you create a ButtonLabelModifier struct and add the styling modifiers.

Next, in the same file, add the following line at the end of extension Text:

func buttonLabel() -> some View {
  modifier(ButtonLabelModifier())
}

Here, you create a buttonLabel() method and add the modifier you created above.

Open AdoptionFormView.swift and replace all the modifiers of Text("Adopt Me") below it with:

.buttonLabel()

Build and run. Your UI looks exactly as it was:

Pet detailed information

While this modifier works well, in some cases, you don’t want to apply the styling only to your button’s label but to the entire button.

Next, you’ll refactor your button’s label modifier and create a custom button style. This also allows you to apply custom styling depending on the button’s isPressed property.

Refactoring ViewModifier Into a ButtonStyle

Open ViewModifiers.swift and replace // TODO: 4 with:

// 1
struct PrimaryButtonStyle: ButtonStyle {
  // 2
  func makeBody(configuration: Configuration) -> some View {
    // 3
    configuration.label
      .font(.title2)
      .padding(.horizontal, 30)
      .padding(.vertical, 8)
      .foregroundColor(Color.pink)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(Color.pink, lineWidth: 1.5)
      )
  }
}

Here’s a breakdown:

  1. You create a PrimaryButtonStyle struct that conforms to ButtonStyle, the type that provides standard behavior and appearance to all buttons in the view hierarchy.
  2. Then, you add a makeBody(configuration:) method that returns some View. The configuration method parameter is of type Configuration. This is just a ButtonStyleConfiguration struct typealias that’s built into SwiftUI. It has a few properties you can use to customize the button style.
  3. One of those properties is the button’s label. By calling configuration.label, you’re explicitly saying you want the modifiers to style the button’s label.

Now you need to apply this button style. When you created the last modifier, you applied it to the button’s label. Now that you created a button style modifier, you need to apply it directly to the button.

Open AdoptionFormView.swift and remove the modifier from Text("Adopt Me"). Add your new modifier below the trailing closure of alert(_:isPresented:actions:):

.buttonStyle(PrimaryButtonStyle())

Build and run. Your UI looks the same as before:

Pet detailed information

Apart from being able to add the button’s label styling, ButtonStyleConfiguration has another property that you’ll learn how to use next.

Using the isPressed Property on Button Style

Right now, when you press the button, you can see the default opacity styling applied. You’ll use the isPressed property to show custom styling when a user presses the button.

Open ViewModifiers.swift and in the PrimaryButtonStyle struct replace the last two modifiers, .foregroundColor() and .overlay(), with the following:

.foregroundColor(
  configuration.isPressed
  ? Color.mint.opacity(0.2)
  : Color.pink
)
.overlay(
  RoundedRectangle(cornerRadius: 8)
    .stroke(
      configuration.isPressed
      ? Color.mint.opacity(0.2)
      : Color.pink,
      lineWidth: 1.5
    )
)

In the code above, you use the configuration.isPressed property to add a different style when the button gets pressed. It changes the label’s color and border’s stroke to mint and adds a 0.2 opacity.

Build and run. Tapping the button changes the color to mint and applies an opacity modifier:

Adopt Me button showing the color change to mint

Now it’s time to go a step further and create a conditional modifier.

Conditional ViewModifiers

Conditional modifiers let you apply a modifier only when a certain condition is true. Unlike view extensions, they can have their own stored properties.

Open EmailValidator.swift. It has a simple regular expression to validate the email address:

EmailValidator class

You’ll create a conditional view modifier to apply a green border to the text field when the user enters a valid email address or apply a secondary color for a default state.

Creating a Conditional ViewModifier

EmailValidator is already set up and connected to the code. You only need to create and apply the conditional modifier.

Open ViewModifiers.swift and replace // TODO: 5 with:

// 1
struct Validate: ViewModifier {
  var value: String
  var validator: (String) -> Bool

  // 2
  func body(content: Content) -> some View {
    // 3
    content
      .border(validator(value) ? .green : .secondary)
  }
}

Here’s a breakdown:

  1. You create a Validate view modifier with two stored properties. value is of type String and it’ll hold an email address that the user enters in the text field. validator is a closure that takes a String and returns a Boolean.
  2. You add the required body(content:) method that returns some View to conform to the ViewModifier protocol.
  3. Finally, you validate the value and apply a green border on the content if validator is true.

You can apply the modifier as-is, but you’ll create an extension on the text field for convenience as you did with previous modifiers.

Open ViewModifiers.swift and replace // TODO: 6 with:

extension TextField {
  func validateEmail(
    value: String,
    validator: @escaping (String) -> (Bool)
  ) -> some View {
    modifier(Validate(value: value, validator: validator))
  }
}

Here, you have properties in the modifier. When you apply the modifier to your UI component, you need to specify these parameters.

Next, open AdoptionFormView.swift and add the following just below TextField("Email address", text: $emailValidator.email):

.validateEmail(value: emailValidator.email) { email in
  emailValidator.isValid(email)
}

Make sure you add it as the first modifier on the text field for the border to work. Remember, modifier order matters.

In the code above, you apply the validateEmail(value:validator:) on the text field. Then you call isValid() in the closure and pass the email for validation.

Build and run. When you type inside the text field, it’ll validate the value as you type. Try typing yourname@mail.com, and you’ll see the border change to green:

TextField changing the color the green when email validator validates correct entry

When you create a view modifier by extending a view, you can supply default parameters. In the next section, you’ll see how this works by creating an Image extension with default parameters.