Home iOS & Swift Tutorials

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.

Version

  • Swift 5.5, iOS 16, Xcode 14

In SwiftUI, you can style your views using modifiers. You can use the built-in modifiers, but also create your own SwiftUI view modifiers. You’re probably already familiar with some of the built-in modifiers, like font() to set a Text view’s font attributes, frame() to constrain the size of a view and padding() to add whitespace around a view’s edges.

If you want to create your own SwiftUI view modifiers, there are two ways to do so. You can create a custom modifier by conforming to the ViewModifier protocol or you can extend View itself. The first approach has one advantage — you’re able to define custom stored properties.

In this tutorial, you’ll use an app called AdoptAPet, a simple master-detail view app, meaning there is a master view that shows high-level information, in this case an adoptable furry friend’s picture and name, and then a detail view that displays when the user taps a list row.

You’ll refactor AdoptAPet as you learn how to use SwiftUI’s ViewModifier protocol for:

  • Creating custom view modifiers.
  • Building new button styles.
  • Creating modifiers with conditional parameters.
  • Extending views to create reusable components.

Getting Started

Download the starter project by clicking Download Materials at the top or bottom of the tutorial.

Open AdoptAPet.xcodeproj inside the starter folder. Build and run to see the app in action:

AdoptAPet app running on iPhone 12 Pro Simulator showing the list of pets and detailed information about a selected pet

AdoptAPet is a simple app with a list of available animals and a detailed view displaying more information about the selected dog or cat.

Views in the app have a lot of styling modifiers, some of which repeat multiple times. This makes your code harder to read and maintain.

You’ll learn how to create custom SwiftUI view modifiers to make your views easier to read. But before that, take a moment to learn about SwiftUI view modifiers in general.

Swifty the bird thinking about different styling to wear and saying Hmmm... How will I modify today?

View Modifiers in SwiftUI

View modifiers are methods you call on your views to customize their appearance and behavior. Views and modifiers work together to define your UI.

You can use view modifiers to:

  • Add accessibility features.
  • Customize the design by applying styling such as colors, fonts and more.
  • Add actions and respond to events.
  • Configure other components, like toolbars and navigation bars.
  • Present other views, like sheets and alerts.

You can modify any type that conforms to View. This includes Text, Button, Image and any custom View you create.

The result of a SwiftUI view modifier is a View. This lets you chain multiple modifiers, listing them immediately one after another, to create another view.

SwiftUI comes with a set of predefined standard modifiers that you can use to design your UI. For example, when you want to add color to a Text view, you can apply the .foregroundColor() modifier.

You are also able to create your own custom modifiers using the ViewModifier protocol. The most common use case for creating custom view modifiers is bundling a set of modifiers you plan to reuse multiple places in your code.

Consider creating a button style and then using it across your app:

Three different tab views of an app showing one button style used across the app and same styling applied in multiple places in the code

You have to repeat your code in multiple places.

Creating your own modifier for a single component that you’ll reuse keeps your code DRY (Don’t Repeat Yourself):

Three different tab views of an app showing one button style used across the app using a view modifier for styling and having the styling code in a single place

It makes it easier to edit the code and gives you a single source of truth. It’s a small change that can make a big difference. Just imagine, the next time designers decide to switch from twenty-point padding down to 18, you won’t have to chase down this change multiple places in your code. It’ll be right there in one neat, easy-to-update place.

Read on to learn how to make this magic time-saving happen by building your own view modifiers.

Building a Title ViewModifier

You don’t think about optimizing your code while writing it. First, you make it work. Then, you do some refactoring.

When styling UI components, look for the lines of code where you repeat modifiers multiple times in different files.

In Xcode, open PetDetailedInformationView.swift and take a look at each of the top-level Text views inside the VStack:

Repeated set of modifiers in multiple places

Every title in the stack has identical styling and a repeated set of modifiers.

There are different approaches you could take here to avoid that repetition.

Every Text and HStack pair are identical. The only differences are each title’s text and the SFSymbol icon and detail property that follow it. You could extract the Text and HStack to a separate view and provide these values, but you still need to call it five times with different parameters.

Instead, you’ll create a view modifier.

Creating a Text ViewModifier

There’s a folder in your project called ViewModifiers and a file with the same name. You’ll create all your modifiers in this file to keep them handy in one place.

Keeping all custom modifiers in a single file makes them easier to maintain and change when needed. Think of it as creating an application-wide style guide. Anyone can refer to this view modifier file if they need to create a UI component that matches the defined app style or if they want to see how a particular look was achieved.

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

// 1
struct DetailedInfoTitleModifier: ViewModifier {
  // 2
  func body(content: Content) -> some View {
    content
      // 3
      .lineLimit(1)
      .font(.title2)
      .bold()
  }
}

Here’s what’s happening:

  1. You create a struct called DetailedInfoTitleModifier that conforms to the ViewModifier protocol. It’s a good idea to provide a descriptive name, especially when you start creating more modifiers.
  2. To conform to ViewModifier, you need to add the body(content:) method that returns some View. The content parameter is the view on which you apply the modifiers.
  3. Finally, you add all your modifiers.

Now, go back to PetDetailedInformationView.swift and replace the three modifiers below Text("Breed") with:

.modifier(DetailedInfoTitleModifier())

Build and run the app. Select a pet, and you’ll see nothing has changed:

Pet detailed information

That’s precisely the outcome you want, and everything looks exactly as intended. You haven’t changed the styling; you’ve only moved the modifiers to a different file.

Now, apply that same modifier to the rest of the titles by replacing existing modifiers below Text("Characteristics"), Text("Size"), Text("Sex") and Text("Age").

Notice how you need to call .modifier(DetailedInfoTitleModifier()) every time you want to apply your modifier. It looks redundant.

To avoid that, you’ll extend the Text to make your modifier accessible in a more convenient way.

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.

View Extensions With Default Parameters

AdoptAPet contains two Image views with identical modifiers but different parameters. This is a perfect candidate to create a view extension modifier with default parameters.

Open PetCardView.swift and you’ll see four modifiers on the Image(pet.photo):

Set of modifiers on Image component

Next, open PetDetailedView.swift and you’ll see the same four modifiers again, but with different parameters for frame(maxWidth:maxHeight):

Set of modifiers on Image component

In the first view, there’s a maxWidth of .infinity. But the second one calculates it using geometry.size.width.

You’ll move them to a new Image extension and provide default parameters that can change when needed.

Creating an Image Modifier

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

extension Image {
  // 1
  func photoStyle(
    withMaxWidth maxWidth: CGFloat = .infinity,
    withMaxHeight maxHeight: CGFloat = 300
  ) -> some View {
    // 2
    self
      .resizable()
      .scaledToFill()
      // 3
      .frame(maxWidth: maxWidth, maxHeight: maxHeight)
      .clipped()
  }
}

Here’s a breakdown:

  1. You create an Image extension with a single method, photoStyle(), that returns some View. photoStyle() takes two parameters, maxWidth and maxHeight. These parameters can differ across views, so you add default values. Both images had maxHeight of 300 so that makes sense as the default. For the maxWidth parameter, set the default value to .infinity.
  2. self refers to the specific view for which you are creating the extension. In this case, an Image. When creating view extensions, self comes in handy. Inside an extension method’s body, you can add different types of views, like HStack and VStack, then use self to indicate where the current view should go.
  3. In the .frame(maxWidth:maxHeight:) modifier, you replace hardcoded values with the method parameters.

Next, open PetCardView.swift and replace all four modifiers below Image(pet.photo) with:

.photoStyle()

There are no parameters specified as this image uses the default parameters.

Now, open PetDetailedView.swift and replace all four modifiers below the Image(pet.photo) with:

.photoStyle(withMaxWidth: geometry.size.width)

To calculate the maxWidth for this image, you need to use GeometryReader, which provides size information about the parent view. So instead of using the default parameter like for maxHeight, you add geometry.size.width.

Build and run. If you’ve done everything right, your app will look exactly as before:

AdoptAPet app running on iPhone 12 Pro Simulator showing the list of pets and detailed information about a selected pet

Amazing! You’ve now learned how to create your own view modifiers and view extensions.

You might be wondering when to use one over the other. There’s no correct answer.

One difference to remember is that view modifiers let you add stored properties while extensions to View don’t.

Ready for a Challenge?

Are you feeling up for a challenge? Want to try and create a view extension on your own? This one is a bit more complex, but nothing you can’t do!

Open PetDetailedInformationView.swift. You’ve created a .detailedInfoTitle() modifier for your titles and cut a few lines of code. But the view is still big.

Not only that, you repeat much of the code five times:

Repeated set of modifiers in a HStack prefixing a title with a different system image

If you look closely, every HStack is almost the same. The only differences are image name and information text. The entire point of the HStack is to add an icon to the section as a prefix.

That’s exactly what your challenge is. Create a new Text extension with a prefixedWithSFSymbol modifier that takes an image name as a parameter. The idea is to create a modifier that’ll prefix the Text with an SF Symbol. Use what you’ve learned above and try to do it yourself.

Once you complete the challenge, you can see a solution here:

[spoiler title=”Challenge Solution”]

extension Text {
  func prefixedWithSFSymbol(named name: String) -> some View {
    HStack {
      Image(systemName: name)
        .resizable()
        .scaledToFit()
        .frame(width: 17, height: 17)
      self
    }
    .padding(.leading, 12)
  }
}

[/spoiler]

Once you create the modifier, you need to apply it to Text. This is how the modifier will change on Text(pet.breed):

[spoiler title=” Applying the modifier”]
Code change after creating a new prefixedWithSFSymbol modifier with old code on the left and a new modifier applied to Text on the right
[/spoiler]

Where to Go From Here?

Well done!

You’ve learned how to create custom SwiftUI view modifiers to make your views reusable and avoid repeating code.

You can download the completed project files by clicking the Download Materials button at the top or bottom of this tutorial.

There’s one SwiftUI view modifier that can come handy that wasn’t covered, called EmptyModifier. It’s used during development to switch modifiers during compile time.

If you want to learn more about how to configure your views, check the Configuring views section of Apple’s SwiftUI documentation.

Once you’ve learned more about view configuration, check out the App Design Apprentice book to learn more about designing modern mobile apps with attractive and effective UI and UX.

We hope you enjoyed this tutorial. If you have any questions or comments, feel free to drop them in the discussion below.

Contributors

Comments

Reviews

More like this