The ability to create custom view modifiers is a powerful feature in SwiftUI. In this article, we will cover examples of how this feature can be used to make building UI so much easier. If you are not familiar with ViewModifiers in SwiftUI and how to create custom ones, you can read about them here
The goal of this article is to cover some of the different ways to create custom modifiers and styles in SwiftUI and how they can be used to make building UI more declarative while still achieving a clean and consistent final output.
The final UI we want to build is this:
Let’s consider all the individual components on the screen:
If you build this screen without any modifiers, the code would look something like this:
struct ContentView: View {
var body: some View {
VStack (alignment: .leading) {
Image("feature")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 220)
.cornerRadius(12)
.padding(.bottom, 12)
Text("Custom ViewModifiers in SwiftUI are the best!")
.foregroundColor(Color("titleTextColor"))
.font(.system(size: 20, weight: .bold))
.padding(.bottom, 12)
Text("Custom ViewModifiers in SwiftUI let you create resuable styles that can be applied to all your views")
.foregroundColor(Color("bodyTextColor"))
.font(.system(size: 14, weight: .medium))
Spacer()
Button(action: {
}) {
Text("Label")
.font(.system(size: 14, weight: .medium))
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.horizontal, 10)
.padding(.vertical, 12)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(12)
}
.padding(.all, 16)
}
}
Styling for some of the elements (the title and details texts for example) would have to be duplicated
Changes to some of the common styling (element padding, corner radius etc) would have to be made in multiple places
Now you could solve this problem the UIKit way by creating custom views, but I'm not a fan of this approach because it involved moving away from the built-in Views and makes onboarding new team members more frictional. An easier way would be to define some universal view modifiers that can be applied instead of the styles themselves.
Let's break down the common styling we need:
Let's start with the corner radius:
struct CommonCornerRadius: ViewModifier {
func body(content: Content) -> some View {
content
.cornerRadius(12)
}
}
This one is rather simple, it allows us to apply a universal corner radius for elements. This makes it easier to change app styles globally without having to create custom Views or having to make multiple changes across the codebase.
struct FullWidthModifier: ViewModifier {
func body(content: Content) -> some View {
content
.frame(minWidth: 0, maxWidth: .infinity)
}
}
This one makes making full-width views easier to implement, no more adding .frame
manually!
struct TitleTextModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundColor(Color("titleTextColor"))
.font(.system(size: 20, weight: .bold))
}
}
struct BodyTextModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundColor(Color("bodyTextColor"))
.font(.system(size: 14, weight: .medium))
}
}
This will allow common text styling, normally you would either create custom Text components or utility functions and add UI components through code.
extension Image {
func aspectFill() -> some View {
self
.resizable()
.aspectRatio(contentMode: .fill)
}
}
Alright, you got me…this isn’t a custom view modifier but a simple extension. This is because ViewModifiers apple to the generic Views and some functions such as resizable
only apply to images, using a combination of extensions and custom modifiers helps get around this.
struct FullWidthButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.fullWidth()
.foregroundColor(Color.white)
.font(.system(size: 14, weight: .medium))
.padding(.horizontal, 10)
.padding(.vertical, 12)
.background(configuration.isPressed ? Color.blue.opacity(0.2) : Color.blue)
}
}
struct FullWidthButton: ViewModifier {
func body(content: Content) -> some View {
content
.buttonStyle(FullWidthButtonStyle())
}
}
Finally, this is for the button, note that while we could have simply created a ViewModifier to accomplish the same effect the button’s appearance would not have changed when tapped. This is because setting .background
on a button forces it to use that background in both tapped and untapped states. ButtonStyle
lets us change the opacity of the button based on whether or not it is pressed.
Now for convenience, I like making extensions that use these modifiers:
extension View {
func commonCornerRadius() -> some View {
modifier(CommonCornerRadius())
}
func fullWidth() -> some View {
modifier(FullWidthModifier())
}
func title() -> some View {
modifier(TitleTextModifier())
}
func body() -> some View {
modifier(BodyTextModifier())
}
func fullWidthButton() -> some View {
modifier(FullWidthButton())
}
}
extension Image {
func aspectFill() -> some View {
self
.resizable()
.aspectRatio(contentMode: .fill)
}
}
struct ContentView: View {
var body: some View {
VStack (alignment: .leading) {
Image("feature")
.aspectFill()
.fullWidth()
.frame(height: 220)
.commonCornerRadius()
.padding(.bottom, 12)
Text("Custom ViewModifiers in SwiftUI are the best!")
.title()
.padding(.bottom, 12)
Text("Custom ViewModifiers in SwiftUI let you create resuable styles that can be applied to all your views")
.body()
Spacer()
Button(action: {
}) {
Text("Awesome")
}
.fullWidthButton()
.commonCornerRadius()
}
.padding(.all, 16)
}
}
Much cleaner! Now at first glance, this feels like more code and effort than simply manually setting the styles but in the long run, this will save a lot of effort. Personally, this approach also encourages your app’s style to be more consistent by relying more on common modifiers than on a view-by-view basis of styling.
And that's about it! Hopefully, this helps you build your apps quicker and easier, another benefit is that these modifiers can be dropped into any of your apps and tweaked to match its style guidelines. I've also been working on a library to take this even further, you can check it out here (PS: At the time of writing this the library is in a super early stage the repo is empty :p but stay tuned)
PS: In my free time I freelance as a mobile dev, if you are looking to hire reach out!