Creating custom modifiers with ViewModifier in SwiftUI

AsyncLearn
4 min readJul 30, 2024

--

The ViewModifier protocol in SwiftUI allows for the creation of custom modifiers to adapt controls according to our preferences. These modifiers can be applied to UI elements in the same way as predefined modifiers. It's common to need a custom control used multiple times within a view or across different views. Instead of duplicating it, we can use ViewModifier to centralize the customization of the control in a single place, thus simplifying our view structure in SwiftUI. Any modification made will automatically apply to all controls without additional effort.

Creating a ViewModifier

To create your first ViewModifier, consider a SwiftUI view called MyView that displays three avatars:

import SwiftUI

struct MyView: View {
var body: some View {
VStack(spacing: 10) {
Circle()
.fill(Color.blue)
.frame(width: 150, height: 150)
.overlay(
Text("AC")
.font(.largeTitle)
.foregroundColor(.white)
)

Circle()
.fill(Color.red)
.frame(width: 150, height: 150)
.overlay(
Text("MC")
.font(.largeTitle)
.foregroundColor(.white)
)

Circle()
.fill(Color.purple)
.frame(width: 150, height: 150)
.overlay(
Text("NL")
.font(.largeTitle)
.foregroundColor(.white)
)
}
}
}

These three elements consist of a Circle and an overlaid Text, with identical features. This is an ideal situation for using ViewModifier. By the end of this article, you will have created a modifier that will turn any Text into an avatar.

To start, create a structure called AvatarModifier that adopts the ViewModifier protocol:

struct AvatarModifier: ViewModifier {
func body(content: Content) -> some View {

}
}

The body function must return a view and takes the content parameter, which refers to the control to which the modifier will be applied. Inside this function, add the following:

// 1
Circle()
.fill(Color.blue)
.frame(width: 150, height: 150)
.overlay(
// 2
content
.font(.largeTitle)
.foregroundColor(.white)
)
  1. Add a Circle with the same modifiers as in the MyView.
  2. Replace the Text with content to reference the control to which the modifier will be applied.

Once done, you can apply AvatarModifier to any control using the modifier modifier, for example:

Text("MC")
.modifier(AvatarModifier())

Update the MyView view using AvatarModifier as follows:

import SwiftUI

struct MyView: View {
var body: some View {
VStack(spacing: 20) {
// 1
Text("AC")
.modifier(AvatarModifier())

Text("MC")
.modifier(AvatarModifier())

Text("NL")
.modifier(AvatarModifier())
}
}
}

Each text uses the AvatarModifier.

Now, the view is simpler, as the control customization has been centralized in a modifier. Additionally, you can use the modifier similarly to SwiftUI’s predefined modifiers by extending the View protocol:

// 1
extension View {
// 2
func avatar() -> some View {
// 3
modifier(AvatarModifier())
}
}
  1. Extend the View protocol.
  2. Create a function called avatar that returns a view.
  3. Return the AvatarModifier.

Replace .modifier(AvatarModifier()) with .avatar() in MyView.

Adding Properties

A notable difference between the current view and the initial one is that each avatar’s background has a fixed color. To allow different background colors, you need to add properties to the AvatarModifier. In AvatarModifier, add the following property above the body function:

let backgroundColor: Color

Then, change .fill(Color.blue) to .fill(backgroundColor). Also, update the avatar function in the View extension to accept the backgroundColor parameter:

extension View {
// 1
func avatar(backgroundColor: Color) -> some View {
modifier(
// 2
AvatarModifier(backgroundColor: backgroundColor)
)
}
}
  1. Add the backgroundColor parameter.
  2. Use backgroundColor in the AvatarModifier.

Finally, in the MyView view, replace the use of .avatar() with .avatar(backgroundColor: YOUR_COLOR), for example:

Text("AC")
.avatar(backgroundColor: .blue)

Thanks to the use of properties, you can customize your modifier with different settings without additional effort. Would you like to add more properties to AvatarModifier? You can include properties to customize the size of the Circle or the type and color of the content font.

Adding State Variables

One advantage of custom modifiers is that you can add state variables, just like in any other SwiftUI view. To demonstrate this, let’s add a boolean state variable to AvatarModifier. Each time an avatar is tapped, it will change its state to produce a simple animation.

Above the backgroundColor property, add the boolean state variable isTapped with the default value false:

@State private var isTapped = false

After the Circle's .overlay modifier, add the following:

// 1
.scaleEffect(isTapped ? 0.9 : 1.0)
// 2
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
  1. Add the .scaleEffect modifier to apply a different scale depending on the isTapped value.
  2. Add the .onTapGesture modifier to detect when the avatar is tapped and change the isTapped value with animation.

When running the code and tapping the avatars, you will see the animation play out:

Complete Code

import SwiftUI

struct MyView: View {
var body: some View {
VStack(spacing: 20) {
Text("AC")
.avatar(backgroundColor: .blue)

Text("MC")
.avatar(backgroundColor: .red)

Text("NL")
.avatar(backgroundColor: .purple)
}
}
}

struct AvatarModifier: ViewModifier {
@State private var isTapped = false

let backgroundColor: Color

func body(content: Content) -> some View {
Circle()
.fill(backgroundColor)
.frame(width: 150, height: 150)
.overlay(
content
.font(.largeTitle)
.foregroundColor(.white)
)
.scaleEffect(isTapped ? 0.9 : 1.0)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}

extension View {
func avatar(backgroundColor: Color) -> some View {
modifier(
AvatarModifier(backgroundColor: backgroundColor)
)
}
}

--

--

AsyncLearn
AsyncLearn

Written by AsyncLearn

Stay up-to-date in the world of mobile applications with our specialised blog.

No responses yet