Articles, podcasts and news about Swift development, by John Sundell.

Passing methods as SwiftUI view actions

Published on 02 Feb 2021
Discover page available: SwiftUI

Often when working with interactive SwiftUI views, we’re using closures to define the actions that we wish to perform when various events occur. For example, the following AddItemView has two interactive elements, a TextField and a Button, that both enable the user to add a new text-based Item to our app:

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: {
                    guard !title.isEmpty else {
    return
}

let item = Item(title: title)
handler(item)
title = ""
                }
            )
            Button("Add") {
                let item = Item(title: title)
handler(item)
title = ""
            }
            .disabled(title.isEmpty)
        }
    }
}

Apart from the leading guard statement within our text field’s onCommit action (which isn’t needed within our button action since we’re disabling the button when the text is empty), our two closures are completely identical, so it would be quite nice to get rid of that source of code duplication by moving those actions away from our view’s body.

One way to do that would be to create our closures using a computed property. That would let us define our logic once, and if we also include the guard statement that our TextField needs, then we could use the exact same closure implementation for both of our UI controls:

private extension AddItemView {
    var addAction: () -> Void {
        return {
            guard !title.isEmpty else {
                return
            }

            let item = Item(title: title)
            handler(item)
            title = ""
        }
    }
}

With the above in place, we can now simply pass our new addAction property to both of our subviews, and we’ve successfully gotten rid of our code duplication, and our view’s body implementation is now much more compact as well:

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: addAction
            )
            Button("Add", action: addAction)
                .disabled(title.isEmpty)
        }
    }
}

While the above is a perfectly fine solution, there’s also another option that might not initially be obvious within the context of SwiftUI, and that’s to use the same technique as when using UIKit’s target/action pattern — by defining our action handler as a method, rather than a closure.

To do that, let’s first refactor our addAction property from before into an addItem method that looks like this:

private extension AddItemView {
    func addItem() {
        guard !title.isEmpty else {
            return
        }

        let item = Item(title: title)
        handler(item)
        title = ""
    }
}

Then, just like how we previously passed our addAction property to both our TextView and our Button, we can now do the exact same thing with our addItem method — which gives us the following implementation:

struct AddItemView: View {
    var handler: (Item) -> Void
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("Add item",
                text: $title,
                onCommit: addItem
            )
            Button("Add", action: addItem)
                .disabled(title.isEmpty)
        }
    }
}

When working with SwiftUI, it’s very common to fall into the trap of thinking that a given view’s layout, subviews and actions all need to be defined within its body, which — if we think about it — is exactly the same type of approach that often led to massive view controllers when working with UIKit.

However, thanks to SwiftUI’s highly composable design, it’s often quite easy to split a view’s body up into separate pieces, which might not even require any new View types to be created. Sometimes all that we have to do is to extract some of our logic into a separate method, and we’ll end up with much more elegant code that’ll be easier to both read and maintain.