Creating a reusable action menu component in SwiftUI
SwiftUI has revolutionised how developers build user interfaces for Apple platforms - its fluent API make creating UIs more approachable and efficient. However, you can often end up with code that is repetitive, tightly coupled to the app’s data model, and generally not well structured.
In this post, I will walk you through the process of building a reusable SwiftUI component that you can seamlessly integrate in your own SwiftUI apps.
For an app I am currently working on , I needed an action menu similar to the one in Apple’s Mail app. Building a view like this is relatively straightforward in SwiftUI - it mostly consists of a couple of Buttons
that are embedded in a List
view that is presented on a sheet.
Here is a simplified version of the code:
This works just fine, but there are a couple of issues:
- All of the code lives in the main view and takes up a lot of space. Just imagine how many lines of code this will occupy if we add more actions!
- The code for the action menu is tightly coupled to the main view - what if we want to implement a similar action menu on a different view? We’d have to copy and paste a lot of the code. If we make changes to the code in one place, we’d have to manually update it in all other places - a maintenance nightmare.
- The styling doesn’t match the look and feel of the action menu in Apple’s Mail.app (with the label on the left, and the icon on the right).
In short - this is not reusable!
In the following, I will show you how to turn this prototype into a reusable SwiftUI component that you can easily import and use in your own applications. Along the way, you will learn how to apply the techniques I teach in my video series Building Reusable SwiftUI Components. By the end of this post, you should have enough knowledge to start building and shipping your own reusable SwiftUI components.
Let’s get started!
Creating a custom view for the action menu
The first step required to turn the initial, non-reusable code snippet into a reusable component is to extract the code for displaying the menu items into a separate view.
The easiest way to achieve this is to create a new file using the SwiftUI view template, naming it ActionMenu
, and then cut / paste the NavigationStack
and all its containing code into the body
of this new view:
Notice that this requires us to pass the array of fruits
and the selected fruit item (i.e., the one the action menu will act on) to ActionMenu
.
The call site looks a lot (c)leaner now which a good step in the right direction. However, ActionMenu
is tightly coupled to the app’s data model now, preventing us from reusing it in other contexts.
Refactoring ActionMenu
for reuse
To make ActionMenu
reusable, we will first introduce a view builder. View builders are one of the key features that power SwiftUI’s declarative syntax, and they are used extensively in SwiftUI’s core components like VStack
, HStack
, List
, and others.
By using a view builder, we can extract all the code we used for creating the contents of the action menu, and pass it in from the call site.
This also means we can remove all references to any application specific types, such as the fruits
and selectedFruit
properties from the ActionMenu
type, since we will be dealing with them at the call site.
This is great, as it makes ActionMenu
more reusable in other contexts.
However, notice that we’re no longer able to call dismiss()
to dismiss the sheet. This is something we’ll have to fix in a later step.
You might be thinking “no problem, we can just bring back the isMoreActionTapped
state property we used in the beginning. That would definitely work, but there’s a better way to implement the sheet dismissal. But before we get there, let’s first improve the developer experience of setting up the action menu.
Creating a view modifier
At the moment, creating the action menu requires us to set up a sheet, and then instantiate the ActionMenu
, making the call site more busy than it should be. Wouldn’t it be nice to able to just write the following:
To make this possible, we will introduce a custom view modifier. In SwiftUI, we use view modifiers like .font
, .padding
, .foregroundColor
and others all the time to specify the look and feel of our SwiftUI views.
But you can also create custom view modifiers to encapsulate (or group) other view modifiers that you routinely use together. Creating the sheet with the enclosed action menu is such a case.
View modifiers typically consist of two parts: firstly, a struct that conforms to the ViewModifier
protocol and contains the code you want to make reusable. Secondly, a convenience function that is defined as an extension on View
, allowing us to use view modifiers in a fluent way by chaining them together.
Let’s first define ActionMenuModifier
, and encapsulate the sheet / action menu structure.
A couple of notes:
content
is the view that you apply the view modifier tomenuContent
represents the sections and buttons making up the action menu. Notice how we’re passingmenuContent
through to theActionMenu
view we defined earlier.- Also notice that we’re using a view builder for the menu content, to match the view builder on the
ActionMenu
view.
To apply a view modifier to a view, you can use the modifier
view modifier (yes, very meta, I know):
I think you will agree with me that approach feels clunky and doesn’t quite achieve the elegant syntax we expect from SwiftUI’s view modifiers.
We can fix this by introducing an extension on View
:
This extension allows us to apply the view modifier to any view in a fluent way by calling actionMenu
:
This approach aligns much better with SwiftUI’s API, offering a similar experience to built-in modifiers like confirmationDialog
, and ultimately provides a smoother developer experience.
Styling the action labels
In the introduction, I mentioned that I wanted to re-create the look and feel of the action menu in Apple’s mail app. If you compare the action menus in Apple’s mail app and the sample app in this blog post, you will notice one main differences: the icons in Apple’s mail app are on the right.
Now, we could re-create this design by creating a custom label like this:
However, this would lead to a lot of code duplication.
Instead, let’s implement a custom style that we can use inside the ActionMenu
view.
Label
, like many other SwiftUI views supports view styling. This means you can customize the entire look and feel of the view by providing a custom style. To customize Label
, we need to implement a custom style that conforms to LabelStyle
.
The main requirement of LabelStyle
is a makeBody
function, and by looking at the following code snippet, you will realise that this function is very similar to the body
function of a regular SwiftUI view - the only difference is that is has a configuration
parameter:
Configuration
is a type that is specific to the view style, and contains the key components of the respective view. For example, here is the definition of LabelStyleConfiguration
:
As you can see, this gives us access to the title
and the icon
being used to display the label, allowing us to implement MenuLabelStyle
as follows:
You might notice we’re using the same layout we prototyped earlier when we considered styling the label of the Button
s manually. Also notice how we’re using configuration.title
and configuration.icon
to get access to those key elements of the Label
view.
To make sure the icon scales proportionally to the title of the view when the user uses dynamic type, we define a scaled metric constant for the icon size.
Finally, to make it more convenient to use this new style, let’s define a constant for this style:
This allows us to instantiate MenuLabelStyle
as follows:
The cool thing about SwiftUI styles is that they’re stored in the SwiftUI environment, and thus will be inherited to all views in the view hierarchy. This means we can apply this new view style to the List
view inside our ActionMenu
view, and it will be applied to all labels inside the list view:
Dismissing the sheet
We’re almost there! There’s just one missing piece: automatically dismissing the sheet after an action is tapped.
To solve this, we can leverage the power of custom button styles in SwiftUI. Remember, custom styles don’t just control the look of a view, but also its behavior!
There are several styles for buttons: DefaultButtonStyle
, BorderlessButtonStyle
, PlainButtonStyle
, BorderedButtonStyle
, BorderedButtonStyle
, and PrimitiveButtonStyle
, but only two button style configurations: PrimitiveButtonStyleConfiguration
and ButtonStyleConfiguration
.
PrimitiveButtonStyleConfiguration
provides access to the button’s role
, label
, and - most importantly - its trigger
, which is what we need to override the button’s action behaviour.
Here is an implementation of ActionMenuButtonStyle
that allows us to dismiss the sheet after the button trigger has been initiated:
A couple of notes:
- We use
configuration.trigger()
to call the buttons action handler. - After that, we
dismiss
the sheet. - To make sure the button’s action is triggered even if the user taps inside the white space between the label and the icon, we set the content shape of the button’s label to
Rectangle
.
Just like we did for the custom label style, let’s declare a function to make applying this new style more fluent:
Finally, we need to apply this new view modifier to the action menu:
That’s it! Now our sheet will automatically close after an action is tapped.
That’s a wrap!
We did it! We built a reusable action menu that looks and behaves just like the one in Apple Mail. 🎉
Along the way, we explored some key SwiftUI techniques that you can apply to your own projects. Here’s a quick recap of what we covered:
- Prototyping and Reusability: We saw how to efficiently prototype views and make them reusable throughout your app.
- Custom View Modifiers: We learned how to create custom view modifiers that seamlessly integrate new views into the view hierarchy.
- View Builders: We discovered how view builders can decouple your views from business logic, making them more flexible and reusable.
- SwiftUI Styles: We explored how to encapsulate custom styling for your views using SwiftUI’s style system.
- Behavioral Customization with Styles: We even saw how custom styles can be used to modify the behavior of views, like automatically dismissing our action sheet.
Hopefully, you found this tutorial helpful and can use these techniques to build even more awesome SwiftUI interfaces!
Where to go from here
Want to dive deeper into the world of reusable SwiftUI components? I’ve got you covered!
- Video Series: Check out my YouTube series, Building SwiftUI Components, for a comprehensive look at the fundamentals. It covers a lot of the basics we didn’t have time for in this post.
- Conference Talks: I’ve also given talks on this topic at various conferences.
- Swift Heroes: Learn how to build a text input field with a floating label.
- SwiftConf: See how to create a reusable avatar component.
- Interactive tutorial: For an interactive, hands-on tutorial, check out the Tutorials page on my blog, which includes a tutorial that walks you through the process of building a reusable avatar component in SwiftUI.
Creating custom SF Symbols using the SF Symbols app
No Design Skills Needed
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause
SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
Styling SwiftUI Views
How does view styling work?
Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views