HIPEventedProperty

Swift 3.1

Docs (you may already be here)

HIPEventedProperty is a simple library of observables, intended to be the minimum viable support for a Model-View-ViewModel (MVVM) architecture. At Hipmunk, we’ve used it for exactly that.

Think of it as ReactiveCocoa stripped way, way down. It’s less than 150 lines long.

Design goals

  • Small stack traces
  • Easily understood memory model
  • Simple, clear code with no complex type system tricks
  • Obvious API

ReactiveCocoa and RxSwift are a lot more powerful, and even more succinct in most cases, but HIPEventedProperty has a tiny API surface.

Examples

HIPEventSource

The most basic class in the module is HIPEventSource. A HIPEventSource collects callbacks that are tied to the memory lifecycle of objects, and allows them to be fired iff their associated objects are still alive.

To restate:

  • Create a HIPEventSource
  • Call source.subscribe(withObject: foo) { doStuff(); }
  • As long as foo is alive (not deallocated by ARC), whenever you call source.fireEvent(), your block will be called.
let p = HIPEventSource()
var x: NSObject? = NSObject()

p.subscribe(withObject: x) {
    print("Event fired")
}

p.fireEvent()  // prints "Event fired"
x = nil
p.fireEvent()  // nothing happens because the `NSObject` was deallocated

Here’s a typical example. Imagine you have a view controller that you want to update whenever some event fires. You’ll want to create an event source, subscribe to it, and let your callback get deallocated when the view controller is deallocated.

class MyViewController: UIViewController {
    let taps = HIPEventSource()
    @IBOutlet var button = UIButton()

    override func viewDidLoad() {
        button.setTitle("Tap", for: .normal)
        button.addTarget(taps, action: #selector(HIPEventSource.fireEvent), for: .touchUpInside)
        _ = taps.subscribe(withObject: self) {
            // It's important not to strongly reference the `withObject:` argument inside the block!
            // Otherwise you might create a reference cycle:
            //     self -> taps -> callback -> self
            [weak self] in  // <------ WEAK SELF

            self?.tapLabel.text += (self?.tapLabel.text ?? "") + " tap"
            // button text becomes "Tap tap tap tap[ ...]"
        }
    }
}

You should read the code of HIPEventSource. It’s only 27 lines.

HIPEventedProperty

Collecting event callbacks is nice, but usually there’s some value associated with events. HIPEventedProperty and its variants deal with values changing over time. There are three versions; which one you use depends on whether the type you want to store is Equatable and/or optional:

  • HIPEventedProperty: Equatable, non-optional, skips duplicate values
  • HIPEventedPropertyOptional: Equatable, optional, skips duplicate values
  • HIPEventedPropertyBasic: No restrictions, no skipping of duplicate values

Note: Most of the documentation for these classes’ methods appears in HIPEventSourceWithValue.

Example: HIPEventedProperty

let x = NSObject()  // example object so our callbacks aren't deallocated

// Track a non-optional integer
let numTaps = HIPEventedProperty<Int>(0)
let unsubscribe = numTaps.subscribeToValue(withObject: x) {
  latestValue in
  print("Tap number: \(latestValue)")
}
numTaps.value += 1  // prints "Tap number: 1"
numTaps.value += 1  // prints "Tap number: 2"
numTaps.value  = 2  // does nothing; value is the same
unsubscribe()
numTaps.value += 1  // does nothing; callback was removed

Example: HIPEventedPropertyOptional

let x = NSObject()  // example object so our callbacks aren't deallocated

// Track an optional integer
let rating = HIPEventedPropertyOptional<Int>(nil)
rating.subscribeToValue(withObject: x) {
  latestValue in
  if let value = latestValue {
    print("Has rating: \(value)")
  } else {
    print("Has no rating")
  }
}
rating.value = nil  // does nothing; value was already nil
rating.value = 1    // prints "Has rating: 1"
rating.value = 1    // does nothing; value was already 1

Example: HIPEventedPropertyBasic

let x = NSObject()  // example object so our callbacks aren't deallocated

// Track a non-equatable class
let asyncImage = HIPEventedPropertyBasic<UIImage?>(nil)
asyncImage.subscribeToValue(withObject: x) {
  maybeImage in
  someImageView.image = maybeImage
  print("Set image to \(maybeImage)")
}
let myImage = UIImage("rick-astley")
asyncImage.value = myImage
asyncImage.value = myImage  // runs block again; no equality check

Contributors