Optimize your SwiftUI code: Two ways to avoid duplication
Nathanaël CHERRIER
Posted on June 10, 2024
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()
}
}
}
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)
}
}
}
}
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))
}
}
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))
}
}
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))
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()
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))
}
}
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.
Posted on June 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.