Passing methods as SwiftUI view actions
Discover page available: SwiftUIOften 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.