Optimize your SwiftUI code: Two ways to avoid duplication

mindsers

Nathanaël CHERRIER

Posted on June 10, 2024

Optimize your SwiftUI code: Two ways to avoid duplication

Optimize your SwiftUI code: Two ways to avoid duplication

Code duplication is a real problem in applications. Having a lot of duplicate code means “complicated maintenance” but also a catastrophic experience.

I have two methods to avoid this problem in my Swift applications. I'll tell you all about them in this article.

Custom components for greater SwiftUI code modularity

As you would with other frameworks, SwiftUI allows you to create views in a declarative/descriptive way.

The views are composed of other views and the pattern can be repeated endlessly on several levels.

struct SignInScreen: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Connect")
            Spacer()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, the SignInScreen view is composed of a VStack view, a Text view and a Spacer view. Same operation therefore as a web framework or the global component will be composed of several smaller components.

Since views are just assemblies of views, we too can create our own views to bring together repeating elements.

struct SignInScreen: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            TextField("Email address", text: $email)

            DividerCustom()

            Button("Connect") {
                // action
            }
        }
    }
}

struct SignUpScreen: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            TextField("Email address", text: $email)

            DividerCustom()

            Button("Sign Up") {
                // action
            }
        }
    }
}

struct DividerCustom: View {
    var body: some View {
        HStack(alignment: .center) {
            VStack {
                Divider()
                    .frame(height: 1.0)
                    .background(.green)
            }
            Text("or")
            VStack {
                Divider()
                    .frame(height: 1.0)
                    .background(.green)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The DividerCustom view combines three other views and thus forms a view that is called in the SignInScreen and SignUpScreen view.

Modifying the DividerCustom code will update the element in both components where it is used. This prevents the code from being duplicated several times in the application.

Optimization with modifiers and ViewModifier

Sometimes, what is duplicated is not a set of components but a set of modifications on a single view.

struct Buttons: View {
    var body: some View {
        Button {
            // action
        } label: {
            Label("Connect with Google", image: "GoogleLogo")
                .frame(maxWidth: .infinity)
        }
        .padding()
        .frame(minWidth: 300, alignment: .center)
        .background(backgroundColor)
        .foregroundColor(foregroundColor)
        .cornerRadius(30)
        .font(.Theme.regular(size: 16))

        Button {
            // action
        } label: {
            Label("Connect with Facebook", image: "FacebookLogo")
                .frame(maxWidth: .infinity)
        }
        .padding()
        .frame(minWidth: 300, alignment: .center)
        .background(backgroundColor)
        .foregroundColor(foregroundColor)
        .cornerRadius(30)
        .font(.Theme.regular(size: 16))

        Button {
            // action
        } label: {
            Label("Connect with GitHub", image: "GitHubLogo")
                .frame(maxWidth: .infinity)
        }
        .padding()
        .frame(minWidth: 300, alignment: .center)
        .background(backgroundColor)
        .foregroundColor(foregroundColor)
        .cornerRadius(30)
        .font(.Theme.regular(size: 16))
    }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we want to display three login buttons for different services.

We don't want to change the behavior of a classic button or even associate it with other views so that they work together. What we want here is to modify the design of the button so that all the buttons look the same.

We will end up with 6 lines of modifications of a button which are duplicated each time a new button is created.

If I ultimately want to change the button's maximum width, I'll have to apply the change manually for each button!

One solution to avoid code duplication (and simplify application maintenance) in this situation is ViewModifiers. This is a very simple way to apply modifiers to multiple views:

struct BasicButton: ViewModifier {
    let backgroundColor: Color
    let foregroundColor: Color

    func body(content: Content) -> some View {
        content
            .padding()
            .frame(minWidth: 300, alignment: .center)
            .background(backgroundColor)
            .foregroundColor(foregroundColor)
            .cornerRadius(30)
            .font(.Theme.regular(size: 16))
    }
}
Enter fullscreen mode Exit fullscreen mode

A ViewModifier is a struct with a body function. It is this function that will apply the styles to our views.
To use it, we use the generic modifier modifier() as follows:

Button {
  // action
} label: {
  Label("Connect with GitHub", image: "GitHubLogo")
    .frame(maxWidth: .infinity)
}
.modifier(BasicButton(backgroundColor: .black, foregroundColor: .white))
Enter fullscreen mode Exit fullscreen mode

And that's it!

BasicButton will apply the styles to our Button view with no more effort.

To make the code a little more elegant, here is a little trick:
And we call this function from our Button view like classic modifiers

Button {
  // action
} label: {
  Label("Connect with GitHub", image: "GitHubLogo")
    .frame(maxWidth: .infinity)
}
.withSteplyDefaultStyle()
Enter fullscreen mode Exit fullscreen mode

We first add a function to the View class that will call the modifier.

extension View {
    func withSteplyDefaultStyle(foregroundColor: Color = .white, backgroundColor: Color = .blue) -> some View {
        modifier(BasicButton(backgroundColor: backgroundColor, foregroundColor: foregroundColor))
    }
}
Enter fullscreen mode Exit fullscreen mode

Note also the use of optional parameters, so you don't have to mention them if they do not change.

You can also limit the use of this modifier to the Button view by extending the Button class rather than View, as we have done here.

The downside is that you will only be able to call the modifier directly on the view. It cannot be chained after other modifiers because this one returns an object of type View and not Button.

Conclusion

As already discussed, avoiding code duplication is not only a good practice to make your code look “pretty”. Rather, it's one of the easiest ways to optimize your code to reduce development time and application maintenance time.
Do not hesitate to use the ViewModifier or the creation of custom components to optimize your SwiftUI code.

💖 💪 🙅 🚩
mindsers
Nathanaël CHERRIER

Posted on June 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related