Last active
December 27, 2018 17:17
-
-
Save pixelrevision/c47027c7e9faf904b8082215093219ce to your computer and use it in GitHub Desktop.
Swift observable protocol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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