Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@pixelrevision
Last active December 27, 2018 17:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pixelrevision/c47027c7e9faf904b8082215093219ce to your computer and use it in GitHub Desktop.
Save pixelrevision/c47027c7e9faf904b8082215093219ce to your computer and use it in GitHub Desktop.
Swift observable protocol
import Foundation
public protocol Observable: class {
associatedtype ObservableTarget = AnyObject
func add(observer: ObservableTarget)
func remove(observer: ObservableTarget)
func dispatch(_ handler: (ObservableTarget) -> ())
}
fileprivate struct AssociatedKeys {
static var observers: UInt8 = 0
}
public extension Observable {
var observers: [ObservableTarget] {
return (objc_getAssociatedObject(self, &AssociatedKeys.observers) as? [ObservableTarget]) ?? []
}
private func setObservers(_ observers: [ObservableTarget]) {
objc_setAssociatedObject(self, &AssociatedKeys.observers, observers, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
func add(observer: ObservableTarget) {
let appendedObservers = observers
.filter { ($0 as AnyObject) !== (observer as AnyObject) } + [observer]
setObservers(appendedObservers)
}
func remove(observer: ObservableTarget) {
let filtered = observers
.filter { ($0 as AnyObject) !== (observer as AnyObject) }
setObservers(filtered)
}
func dispatch(_ handler: (ObservableTarget) -> ()) {
observers.forEach { handler($0) }
}
}
import XCTest
class ObservableTests: XCTestCase, ObservableTestingDelegate {
func testAddObservable() {
let observable = TestObservable()
observable.add(observer: self)
let observable2 = TestObserver()
observable.add(observer: observable2)
// make sure we dispatch twice
var count = 0
observable.dispatch { target in
count += 1
}
XCTAssert(count == 2)
// make sure adding twice does not double up
observable.add(observer: observable2)
count = 0
observable.dispatch { target in
count += 1
}
XCTAssert(count == 2)
// make sure removed
observable.remove(observer: observable2)
count = 0
observable.dispatch { target in
count += 1
}
XCTAssert(count == 1)
}
func testFullfillment() {
// make sure delegate gets called
let observable = TestObservable()
let observer = TestObserver()
observable.add(observer: observer)
XCTAssertFalse(observer.hasBeenCalledAsDelegate)
observable.dispatch { target in
target.sampleCall()
}
XCTAssert(observer.hasBeenCalledAsDelegate)
}
func testMemoryExpectations() {
var observable: TestObservable? = TestObservable()
var observer: TestObserver? = TestObserver()
let weakObserver = Weak(observer!)
observable?.add(observer: observer!)
observer = nil
// make sure that is the observable is removed it does not hold onto a weak referenced value
XCTAssertNotNil(weakObserver.value)
observable = nil
XCTAssertNil(weakObserver.value)
}
func sampleCall() {
XCTAssert(true)
}
}
// MARK: utils
fileprivate protocol ObservableTestingDelegate: class {
func sampleCall()
}
fileprivate extension ObservableTestingDelegate {
func sampleCall() {}
}
fileprivate class TestObservable: Observable {
typealias ObservableTarget = ObservableTestingDelegate
}
fileprivate struct Weak<T: AnyObject> {
weak var value: T?
init(_ value: T) {
self.value = value
}
}
fileprivate class TestObserver: ObservableTestingDelegate {
var hasBeenCalledAsDelegate = false
func sampleCall() {
hasBeenCalledAsDelegate = true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment