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
Circle
with the same modifiers as in theMyView
. - Replace the
Text
withcontent
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())
}
}
- Extend the
View
protocol. - Create a function called
avatar
that 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: 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)
)
}
}
- Add the
backgroundColor
parameter. - Use
backgroundColor
in 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 = false
After the Circle
's .overlay
modifier, add the following:
// 1
.scaleEffect(isTapped ? 0.9 : 1.0)
// 2
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
- Add the
.scaleEffect
modifier to apply a different scale depending on theisTapped
value. - Add the
.onTapGesture
modifier to detect when the avatar is tapped and change theisTapped
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)
)
}
}