Simplifying communication patterns with closure in Swift

Khoa Pham
Khoa Pham
May 29 · 10 min read
Thanks to Jardson Almeida for the inspiration

Table of Contents

The many communication patterns

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) {}
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

Detecting object deallocation

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

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

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

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

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

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

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

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)
}
}

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing

Khoa Pham

Written by

Khoa Pham

Cageball apprentice at @hyperoslo. My apps https://onmyway133.github.io/

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing