Event handing in Swift

Passing events around is at the core of UI development. Understanding how it’s changed through the years will help you appreciate how far we’ve come.

Indirect Object Ownership

Way back, the standard way to own objects across viewControllers and views was to use weak variables. This was before Combine or RX. UIKit users will recognize this pattern:

class Counter {
	var count: Int
}
class CounterVC: ViewController {
	let counter = Counter()
	let upButtonView = UpButtonView()

	override func viewDidLoad() {
		super.viewDidLoad()
		upButtonView.counter = counter
	}
}
class UpButtonView: View {
	weak var counter: Counter?

	@objc func didPressUpButton() {
		counter?.count += 1
	}
}

Alternatively, weak delegates to consolidate event handling at the controller level:

class CounterVC: ViewController {
...
	override func viewDidLoad() {
		...
		upButtonView.delegate = self
	}
}
extension CounterVC: UpButtonViewDelegate {
	func didPressUp() {
		counter.count += 1
	}
}
protocol UpButtonViewDelegate: AnyObject {
	func didPressUp()
}
class UpButtonView: View {
	weak var delegate: UpButtonViewDelegate?

	@objc func didPressUpButton() {
		delegate?.didPressUp()
	}
}

This verbose pattern to send events around was explicitly representative of the legacy Objective-C patterns that preceded it.

Combine

Combine was a solution to reducee the hard-to-read verbosity of event handling with weak delegates. Objects can now publish and subscribe to events.

class CounterVC: ViewController {
	let counter = CurrentValueSubject<Int, Never>()
	...

	override func viewDidLoad() {
		...
		upButtonView.counter = counter
		counter.count.sink { ... }
	}
}
class UpButtonView: View {
	weak var counter: CurrentValueSubject<Int, Never>

	@objc func didPressUpButton() {
		counter.send(counter.value + 1)
	}
}

SwiftUI

The latest iteration to work with SwiftUI is using @Published @StateObject and @ObservedObject.

class Counter {
	@Published var count: Int
}
struct CounterView: View {
	@StateObject var counter = Counter()
	var body: some View {
        VStack {
            Text("Your score is \(progress.score)")
            UpButtonView(counter: counter)
        }
    }

}
struct UpButtonView: View {
	@ObservedObject var counter

 	var body: some View {
		Button("Increase Score") {
		progress.score += 1
		}
	}
}

Here, @ObservedObject acts as a weak reference, while @StateObject expresses its ownership across re-renders.

The new keywords are syntactic wrappers under the hood, but are now easier to read without leaking too much abstraction.


Inspired in part by HackingWithSwift’s article on @ObservedObject.