SwiftUI Custom View Modifiers

Create your own custom SwiftUI view modifier when you want to reuse a set of modifiers on multiple views. Removing duplication also cleans up and improves the readability of your SwiftUI views.

Here’s my starting point. A SwiftUI view that contains two text views with some styling applied:

struct CountryView: View {
  let country: Country
  
  var body: some View {
    VStack(alignment: .leading, spacing: 4) {
      Text(country.name)
        .font(.headline)
        .padding([.leading,.trailing], 5)
        .background(Color(.secondarySystemBackground))
        .cornerRadius(5.0)
                  
      Text(country.formattedPopulation)
        .font(.caption)
        .padding([.leading,.trailing], 5)
        .background(Color(.secondarySystemBackground))
        .cornerRadius(5.0)
    }
  }
}

Here’s how it looks when previewed in light and dark modes:

United Kingdom. 66,488,991. Caption style in light and dark mode

There’s a lot of repetition in my view. The two text views each have the same four view modifiers to set the font (albeit a different font), background, padding and corner radius. We can clean this up by combining those four modifiers into our own custom modifier that we can then apply to any view.

What Is A View Modifier?

A ViewModifier takes a view, modifies it in some way and returns the result. As we’ve already seen you can concatenate a chain of modifiers to build a final view. To create our own custom view modifier we adopt the ViewModifier protocol in our own type.

Here’s my first go at creating a Caption view modifier:

// Caption.swift
import SwiftUI

struct Caption: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(.caption)
      .padding([.leading,.trailing], 5)
      .background(Color(.secondarySystemBackground))
      .cornerRadius(5.0)
  }
}

There’s only one requirement, to implement the body method that accepts some content and returns a view. I copied the modifiers from the country view, applying them to the content. We’ll see how to customize the font in a moment.

We could apply our Caption modifier to a view using modifier(_:):

Text("Hello World!")
  .modifier(Caption())

That’s a bit ugly. A better way is to add our custom modifier as an extension on View:

extension View {
  func caption() -> some View {
    modifier(Caption())
  }
}

We can now apply our modifier directly like the standard SwiftUI modifiers:

Text("Hello World!")
  .caption()

Hello World! in a filled-rounded rectangle in light and dark modes

Adding Parameters

My caption modifier always uses a .caption font but in my original view I also used a .headline font. Let’s give my Caption struct a font property and use it to set the font in the body. Let’s also make the color configurable at the same time:

struct Caption: ViewModifier {
  let font: Font
  let backgroundColor: Color
 
  func body(content: Content) -> some View {
    content
      .font(font)
      .padding([.leading,.trailing], 5.0)
      .background(backgroundColor)
      .cornerRadius(5.0)
  }
}

We need to update our view extension to set the font and color when creating a new Caption. I’ve used this opportunity to set some defaults:

extension View {
  func caption(font: Font = .caption, 
    backgroundColor: Color = Color(.secondarySystemBackground))
    -> some View {
      modifier(Caption(font: font, 
            backgroundColor: backgroundColor))
  }
}

We can now override the default caption font and background color when we apply the modifier. My original country view using the caption modifier:

struct CountryView: View {
  let country: Country
  
  var body: some View {
    VStack(alignment: .leading, spacing: 4) {
      Text(country.name)
        .caption(font: .headline)
                  
      Text(country.formattedPopulation)
        .caption()
    }
  }
}

Note that our modifier works with other views, and modifiers, not just Text:

Image(systemName: "person.fill")
  .foregroundColor(.green)
  .caption(font: .largeTitle)

Large green, filled person symbol using caption style in light and dark modes

Adding To The Xcode Library

For bonus points, we can add our view modifier to the Xcode Library:

@available(iOS 14.0, *)
struct ModifierLibrary: LibraryContentProvider {
  @LibraryContentBuilder
  func modifiers(base: Text) -> [LibraryItem] {
    LibraryItem(base.caption(), category: .effect)
  }
}

Adding To A Swift Package

View modifiers are a great candidate for inclusion in a Swift package either to share between targets in a project or for reuse across projects. Often that needs no more than marking the view extension with public. My final packaged version:

// Caption.swift
import SwiftUI

struct Caption: ViewModifier {
  let font: Font
  let backgroundColor: Color

  func body(content: Content) -> some View {
    content
      .font(font)
      .padding([.leading, .trailing], 5.0)
      .background(backgroundColor)
      .cornerRadius(5.0)
  }
}

extension View {
  /// Place this view in a filled, rounded rectangle.
  /// - Parameters:
  ///   - font: Font applied to view. Default: `.caption`
  ///   - backgroundColor: Background color of view.
  ///     Default: `.secondarySystemBackground`
  public func caption(font: Font = .caption, 
           backgroundColor: Color = Color(.secondarySystemBackground))
    -> some View {
    modifier(Caption(font: font,
          backgroundColor: backgroundColor))
  }
}

@available(iOS 14.0, *)
struct ModifierLibrary: LibraryContentProvider {
  @LibraryContentBuilder
  func modifiers(base: Text) -> [LibraryItem] {
    LibraryItem(base.caption(), category: .effect)
  }
}

See Add resources to Swift packages if your view modifier needs an asset catalog or other resources added to the package.