Cover image

SwiftUI: Reusable UI with Custom Modifiers

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 it here

The goal with 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:

goal

Let’s consider all the individual components on the screen:

Plain SwiftUI code

#

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)
    }
}

There are a couple problems with this approach:

Now you could solve this problem the UIKit way by creating custom views, but personally Im 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.

Lets break down the common styling we need:

Custom View Modifiers

#

Lets 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 adding 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 simple 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 state. 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)
    }
}

Now lets convert the code to use these instead of styling directly:

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 thats 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.