Rambling Intro
One essential aspect of mobile engineering is communication between different components. We all know that building an interactive app requires us to use a good communication pattern between child and parent components. In the good days of UIKit, we all leveraged the delegate - protocol pattern a lot to accomplish this goal, but now with the slow shift away from imperative programming we know that the delegate protocol pattern is no longer an option, unless you’re interfacing with a UIViewRepresentable
for one of those archaic UIKit modules lol (I still ❤️ love UIKit)
. Now, we’re in the realm of SwiftUI, where we use an array of property wrappers, such as @Binding
, to orchestrate these communication ballet. But hang on a second! Sometimes, the full-blown two-way connection that a property wrapper like @Binding
offers is simply an overzealous choice. Occasionally, all we desire is to pass the darn action from a child view to its parent, nothing more, nothing less. Well in such scenarios, I’m a huge fan of turning to closures! 😄
Understanding the Need for Callbacks
In SwiftUI, we often compose views hierarchically. This means we have parent views containing child views, which might contain more child views, forming a tree-like structure. Sometimes, we need child views to just notify their parent views when certain actions occur, such as button taps. This is where callbacks come into play.
Callbacks are closures or functions that are defined and invoked in the child view and then passed up to the parent view for notification of the event. The child view will usually invoke the callback when we need it to communicate something to her parent. This pattern is particularly powerful when combined with enumerations, and i will show you how.
Callbacks + Enumerations
1
2
3
4
5
6
enum PhotoCellEvents {
case onInfo
case onEdit(Photo, Bool)
case onSelect(Photo)
case onDelete(Photo)
}
An enum as we all know defines a common type for a group of related values. Using enums here is really neat because not only can we use it to delegate actions, enums also allow us to pass associated values, so in fact we are using it like you would use a protocol to delegate a set of actions to a parent controller in UIKit. And here we just have a simple example of an enum representing events in a photo cell:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import SwiftUI
struct Photo {
var id: UUID = UUID()
var name: String
}
struct PhotoListView: View {
@State private var photos: [Photo] = [
Photo(name: "Photo 1"),
Photo(name: "Photo 2"),
Photo(name: "Photo 3")
]
var body: some View {
List(photos, id: \.id) { photo in
PhotoCell(photo: photo) { event in
handlePhotoCellEvent(event, for: photo)
}
}
}
private func handlePhotoCellEvent(_ event: PhotoCellEvents, for photo: Photo) {
switch event {
case .onInfo:
// Handle info button tap
print("Info button tapped for \(photo.name)")
case let .onEdit(_, isEditing):
// Handle edit button tap
print("Edit button tapped for \(photo.name). Editing: \(isEditing)")
case .onSelect:
// Handle select button tap
print("Select button tapped for \(photo.name)")
case .onDelete:
// Handle delete button tap
if let index = photos.firstIndex(where: { $0.id == photo.id }) {
photos.remove(at: index)
}
}
}
}
struct PhotoCell: View {
let photo: Photo
var actionCallback: (PhotoCellEvents) -> Void
var body: some View {
HStack {
Text(photo.name)
Spacer()
Button("Info") {
actionCallback(.onInfo)
}
Button("Edit") {
actionCallback(.onEdit(photo, true))
}
Button("Select") {
actionCallback(.onSelect(photo))
}
Button("Delete") {
actionCallback(.onDelete(photo))
}
}
}
}
In this example, we have two views: PhotoListView (the parent) and PhotoCell (the child). The child view accepts a PhotoCellEvents enumeration and a callback closure. Depending on the event, the child view triggers the corresponding action by invoking the callback which the parent view’s handlePhotoCellEvent
will respond to accordingly.
Conclusion
I really love this pattern of one way delegation because of swift’s trailing closure syntax, and by leveraging this combo of enum with callbacks we can easily delegate actions to parent views without defaulting to reactive property wrappers especially when we are not dealing with states or any sort of pub sub.
Hopefully by reading this noob article the next time you need to handle communication between your SwiftUI components you’ll consider using enumerable callbacks to delegate the actions and create more robust and maintainable code.
Thanks for reading, till next time my friends ✌️