Toolbars are an essential part of our iOS app’s user interfaces, providing users with quick access to common actions. However, toolbar code can become a tangle of nested closures in our view’s body, making readability a problem. So, very often, we need to refactor our toolbars into their own ToolbarContentBuilders
methods, or in this case, as we will explore how to refactor them into their own neat, reusable structs. We’ll walk through the process of creating a custom toolbar with cancel and done buttons, and demonstrate how to integrate it into a SwiftUI view.
Getting Started
We’ll start by defining the necessary foundation that will power our custom toolbar’s actions and then implement the custom toolbar.
1
2
3
4
enum CustomToolBarAction {
case cancel
case done
}
We’ve defined the CustomToolBarAction
` enum to represent the available toolbar actions: cancel and done. Next, we need to implement the methods to handle the toolbar actions. The following code is how we will handle these actions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension ContentView {
private func handleToolbarAction(_ action: CustomToolBarAction) {
switch action {
case .cancel:
cancelAction()
case .done:
doneAction()
}
}
private func cancelAction() {
print("Cancel action")
}
private func doneAction() {
print("Done action")
}
}
Creating the Custom Reusable Toolbar
The central component of our custom reusable toolbar is going to be our CustomToolBarContent
struct, which conforms to ToolbarContent
. And here is what it looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct CustomToolBarContent: ToolbarContent {
private let cancelTitle: String
private let doneTitle: String
private let callback: (CustomToolBarAction) -> Void
init(cancelTitle: String, doneTitle: String, callback: @escaping (CustomToolBarAction) -> Void) {
self.cancelTitle = cancelTitle
self.doneTitle = doneTitle
self.callback = callback
}
var body: some ToolbarContent {
ToolbarItem(placement: .principal) {
Text("Title")
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(cancelTitle) {
callback(.cancel)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(doneTitle) {
callback(.done)
}
}
}
}
Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.toolbar {
CustomToolBarContent(cancelTitle: "Cancel", doneTitle: "Done") { action in
handleToolbarAction(action)
}
}
}
}
private func handleToolbarAction(_ action: CustomToolBarAction) {
switch action {
case .cancel:
cancelAction()
case .done:
doneAction()
}
}
private func cancelAction() {
print("Cancel action")
}
private func doneAction() {
print("Done action")
}
}
Closing Thoughts
And just like that, we have abstracted messy toolbar code away from our view’s body to a neat, reusable struct. Following this same pattern, you can see how this would be easy to generalize beyond just two simple toolbar buttons. We can effortlessly standardize our app’s toolbar styling into a reusable component and then pass in the toolbar actions we need. Pretty neat, isn’t it? Thanks for reading.