Simplifying communication patterns with closure in Swift

Khoa Pham
Khoa Pham
May 29, 2019 · 10 min read
Image for post
Image for post
Thanks to Jardson Almeida for the inspiration

Table of Contents

The many communication patterns

In iOS, we can handle button tap by using addTarget which is a function that any UIControl has:

let button = UIButton()
button.addTarget(self, action: #selector(buttonTouched(_:)), for: .touchUpInside)
@objc func buttonTouched(_ sender: UIButton) {}
button.target = self
button.action = #selector(buttonTouched(_:))
@objc func buttonTouched(_ sender: NSButton) {}
Image for post
Image for post
https://www.objc.io/issues/7-foundation/communication-patterns/
button.on.tap { print("button was tapped" }
user.on.propertyChange(\.name) { print("name has changed to \($0)"}
tableView.on.tapRow { print("row \($0) was tapped" }

Associated objects

With Objective C category and Swift extension, we can’t add a stored property, that’s where associated objects come into play. The associated object allows us to attach other objects to the lifetime of any NSObject , with these 2 free functions objc_getAssociatedObject and objc_setAssociatedObject .

Detecting object deallocation

Since the associated object is attached to the host object, there’s this very handy use case that allows detection of the life cycle of the host object. We can, for example, observe UIViewControllerto tell if it has really been deallocated.

class Notifier {
deinit {
print("host object was deinited")
}
}
extension UIViewController {
private struct AssociatedKeys {
static var notifier = "notifier"
}
var notifier: Notifier? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.notifier) as? Notifier
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.notifier,
newValue as Notifier?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
var viewController: UIViewController? = UIViewController()
viewController?.notifier = Notifier()
viewController = nil
XCTAssertNil(viewController?.notifier)

Confusing code completion

If we’re gonna make the on extension to UIButton, UITableView, UIViewController we have to add the associated object into NSObject for all these classes to have on property.

class On {
func tap(_ closure: () -> Void) {}
func tapRow(_ closure: () -> Void) {}
func textChange(_ closure: () -> Void) {}
}
extension NSObject {
private struct AssociatedKeys {
static var on = "on"
}
var on: On? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.on) as? On
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.on,
newValue as On?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
button.on.textChange {}

Protocol with associated type

To remedy this awkward API, we can use a very sweet feature of Swift called protocol with an associated type. We start by introducing EasyClosureAware that has a host EasyClosureAwareHostType of type AnyObject. This means that this protocol is for any class that wants to attach itself to a host object.

private struct AssociatedKey {
static var key = "EasyClosure_on"
}
public protocol EasyClosureAware: class {
associatedtype EasyClosureAwareHostType: AnyObject
var on: Container<EasyClosureAwareHostType> { get }
}
extension EasyClosureAware {
public var on: Container<Self> {
get {
if let value = objc_getAssociatedObject(self, &AssociatedKey.key) as? Container<Self> {
return value
}
let value = Container(host: self)
objc_setAssociatedObject(self, &AssociatedKey.key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return value
}
}
}
extension NSObject: EasyClosureAware { }
public class Container<Host: AnyObject>: NSObject {
public unowned let host: Host

public init(host: Host) {
self.host = host
}

// Keep all targets alive
public var targets = [String: NSObject]()
}
public extension Container where Host: UIButton {
func tap(_ action: @escaping Action) {
let target = ButtonTarget(host: host, action: action)
targets[ButtonTarget.uniqueId] = target
}
}
class ButtonTarget: NSObject {
var action: Action?
init(host: UIButton, action: @escaping Action) {
super.init()
self.action = action
host.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
// MARK: - Action@objc func handleTap() {
action?()
}
}
button.on.tap {}
textField.on. // ummh?
public extension Container where Host: UITextField {
func textChange(_ action: @escaping StringAction) {
let target = TextFieldTarget(host: host, textAction: action)
targets[TextFieldTarget.uniqueId] = target
}
}
class TextFieldTarget: NSObject {
var textAction: StringAction?
required init(host: UITextField, textAction: @escaping StringAction) {
super.init()
self.textAction = textAction
host.addTarget(self, action: #selector(handleTextChange(_:)), for: .editingChanged)
}
// MARK: - Action@objc func handleTextChange(_ textField: UITextField) {
textAction?(textField.text ?? "")
}
}

Action to RxSwift observable

Having button.on.tap {} is nice, but it would be great if that can transform into Observablefor some RxSwift fans like me.

final class RxButton: UIButton {
let tap = PublishSubject<()>()
override init(frame: CGRect) {
super.init(frame: frame)
self.on.tap { tap.onNext(()) }
}
}
button.tap.subscribe(onNext: {})

Timer keeps a strong reference to its target

Unlike target-action in UIControl where target is held weakly, Timer keeps its target strongly to deliver tick event.

func schedule() {
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(timeInterval: 20, target: self,
selector: #selector(self.timerDidFire(timer:)), userInfo: nil, repeats: false)
}
}
@objc private func timerDidFire(timer: Timer) {
print(timer)
}
public extension Container where Host: Timer {func tick(_ action: @escaping Action) {
self.timerTarget?.action = action
}
}
class TimerTarget: NSObject {
var action: Action?
@objc func didFire() {
action?()
}
}
public extension Timer {
static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool) -> Timer {
let target = TimerTarget()
let timer = Timer.scheduledTimer(timeInterval: interval,
target: target,
selector: #selector(TimerTarget.didFire),
userInfo: nil,
repeats: repeats)
timer.on.timerTarget = target
return timer
}
}
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true)
timer.on.tick { print("tick") }
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { timer in
print(timer)
}
}

Key-value observing in Swift 5

Key-value observing is the ability to watch property change for an NSObject , before Swift 5 the syntax is quite verbose and error-prone with addObserver and observeValuem methods. This is not to mention the usage of context , especially in the subclassing situation, where we need a context key to distinguish between observations of different objects on the same keypath.

public extension Container where Host: NSObject {func observe(object: NSObject, keyPath: String, _ action: @escaping AnyAction) {
let item = KeyPathTarget.Item(object: object, keyPath: keyPath, action: action)
keyPathTarget.items.append(item)
object.addObserver(keyPathTarget, forKeyPath: keyPath, options: .new, context: nil)
}
func unobserve(object: NSObject, keyPath: String? = nil) {
let predicate: (KeyPathTarget.Item) -> Bool = { item in
return item.object === object
&& (keyPath != nil) ? (keyPath! == item.keyPath) : true
}
keyPathTarget.items.filter(predicate).forEach({
object.removeObserver(keyPathTarget, forKeyPath: $0.keyPath)
})
keyPathTarget.items = keyPathTarget.items.filter({ !predicate($0) })
}
}
class KeyPathTarget: NSObject {
class Item {
let object: NSObject
let keyPath: String
let action: AnyAction
init(object: NSObject, keyPath: String, action: @escaping AnyAction) {
self.object = object
self.keyPath = keyPath
self.action = action
}
}
var items = [Item]()deinit {
items.forEach({ item in
item.object.removeObserver(self, forKeyPath: item.keyPath)
})
items.removeAll()
}
// MARK: - KVO
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard let object = object as? NSObject,
let keyPath = keyPath,
let value = change?[.newKey] else {
return
}
let predicate: (KeyPathTarget.Item) -> Bool = { item in
return item.object === object
&& keyPath == item.keyPath
}
items.filter(predicate).forEach({
$0.action(value)
})
}
}
let observer = NSObject()
observer.on.observe(object: scrollView: keyPath: #keyPath(UIScrollView.contentSize)) { value in print($0 as? CGSize)}
@objc class User: NSObject {
@objc dynamic var name = "random"
}

let thor = Person()
thor.observe(\User.name, options: .new) { user, change in
print("User has a new name \(user.name)")
}
thor.name = "Thor"

Block-based Notification Center

NotificationCenter is the mechanism to post and receive notification system-wide. Starting from iOS 4, NotificationCenter got its block API addObserverForName:object:queue:usingBlock:

public extension Container where Host: NSObject {func observe(notification name: Notification.Name,
_ action: @escaping NotificationAction) {
let observer = NotificationCenter.default.addObserver(
forName: name, object: nil,
queue: OperationQueue.main, using: {
action($0)
})
notificationTarget.mapping[name] = observer
}
func unobserve(notification name: Notification.Name) {
let observer = notificationTarget.mapping.removeValue(forKey: name)
if let observer = observer {
NotificationCenter.default.removeObserver(observer as Any)
notificationTarget.mapping.removeValue(forKey: name)
}
}
}
class NotificationTarget: NSObject {
var mapping: [Notification.Name: NSObjectProtocol] = [:]
deinit {
mapping.forEach({ (key, value) in
NotificationCenter.default.removeObserver(value as Any)
})
mapping.removeAll()
}
}
viewController.on.observe(notification: Notification.Name.UIApplicationDidBecomeActive) { notification in
print("application did become active")
}

viewController.on.unobserve(notification: Notification.Name.UIApplicationDidBecomeActive)

Where do we go from here

We’ve learned how to make use of associated objects and make nicer APIs. EasyClosure is designed to be extensible and we can wrap any communication patterns. KVO and NotificationCenter APIs have become better starting iOS 10 and Swift 5, and we see a trend of more closure based API as they are declarative and convenient. When we can, we should stick to the system APIs as much as possible and only make our sugar when needed.

func allOn() -> Bool {
return [good, cheap, fast].filter({ $0.isOn }).count == 3
}

good.on.valueChange { _ in
if allOn() {
fast.setOn(false, animated: true)
}
}

cheap.on.valueChange { _ in
if allOn() {
good.setOn(false, animated: true)
}
}

fast.on.valueChange { _ in
if allOn() {
cheap.setOn(false, animated: true)
}
}
Image for post
Image for post

Flawless iOS

🍏 Community around iOS development, mobile design, and…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store