Home Simplifying SwiftUI Toolbars. A Guide to Neat and Reusable Toolbar Code
Post
Cancel

Simplifying SwiftUI Toolbars. A Guide to Neat and Reusable Toolbar Code

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.

This post is licensed under CC BY 4.0 by the author.