Creating custom modifiers with ViewModifier in SwiftUI
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)
)- Add a
Circlewith the same modifiers as in theMyView. - Replace the
Textwithcontentto 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())
}
}- Extend the
Viewprotocol. - Create a function called
avatarthat returns a view. - 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: ColorThen, 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)
)
}
}- Add the
backgroundColorparameter. - Use
backgroundColorin theAvatarModifier.
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 = falseAfter the Circle's .overlay modifier, add the following:
// 1
.scaleEffect(isTapped ? 0.9 : 1.0)
// 2
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}- Add the
.scaleEffectmodifier to apply a different scale depending on theisTappedvalue. - Add the
.onTapGesturemodifier to detect when the avatar is tapped and change theisTappedvalue 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)
)
}
}