Skip to content

Instantly share code, notes, and snippets.

@flyinghyrax
Last active August 29, 2015 14:27
Show Gist options
  • Save flyinghyrax/a07c53544766ebfbb8f4 to your computer and use it in GitHub Desktop.
Save flyinghyrax/a07c53544766ebfbb8f4 to your computer and use it in GitHub Desktop.
Copy into a playground and open the debug area with Cmd+Shift+Y to see output
import Foundation
/*
Demonstrates creating and avoiding retention cycles in callbacks
*/
/* UTILITIES */
/// Wraps a something in a class
class Box<T> {
var value: T
init(value: T) {
self.value = value
}
}
/* OBSERVABLE */
typealias Listener = () -> Void
typealias ListenerBox = Box<Listener>
/// Base class for notifications
class Observable: NSObject {
private var listeners = [ListenerBox]()
func subscribe(listener: Listener) -> AnyObject {
let box = Box(value: listener)
listeners.append(box)
return box
}
func unsubscribe(listener: AnyObject) {
for idx in 0..<listeners.count {
// this is why we wrap the callbacks in a class...
if listeners[idx] === listener {
listeners.removeAtIndex(idx)
// no break, in case you were silly and registered more than once
}
}
}
func notifyObservers() {
for box in listeners {
box.value()
}
}
}
class ConcreteObservable: Observable {
static let instance = ConcreteObservable()
func change() {
print("< Observable changed >")
notifyObservers()
}
}
let observable = ConcreteObservable.instance
/* OBSERVERS */
protocol Observer {
func watch()
}
class ConcreteObserver {
init() {
print("| \(self.dynamicType) created |")
}
func shout() {
print("> Shout! \(self.dynamicType) <")
}
deinit {
print("| \(self.dynamicType) deallocated |")
}
}
/**
Demonstrates a retention cycle.
This will never be deallocated unless it explictly unsubscribes from the Observable
*/
class ConcreteObserverBad: ConcreteObserver, Observer {
func watch() {
ConcreteObservable.instance.subscribe({
self.shout()
})
}
}
/**
Uses a weak reference to self in the callback to avoid a cycle.
*/
class ConcreteObserverBetter: ConcreteObserver, Observer {
func watch() {
ConcreteObservable.instance.subscribe({[weak self] in
print("... in better observer callback")
self?.shout()
})
}
}
class ConcreteObserverBest: ConcreteObserver, Observer {
var listener: AnyObject?
func watch() {
listener = ConcreteObservable.instance.subscribe({[weak self] in
print("...in best observer callback")
self?.shout()
})
}
deinit {
listener.flatMap({ ConcreteObservable.instance.unsubscribe($0) })
}
}
/* Demonstrate the retention cycle... */
print("")
var observer: Observer? = ConcreteObserverBad()
// no-op, as expected
observable.change()
observer!.watch()
// Shouts, as expected
observable.change()
// notice that deinit is never called: the observer is not deallocated
observer = nil
// notice that the callback is still executed, even though we have nilled our only reference to the object that registered that callback!
observable.change()
/* Now without the cycle */
print("")
observer = ConcreteObserverBetter()
observer!.watch()
// The clean observer is notified as expected, but so is the old one, which was never
// deinitialized and never explicitly unsubscribed
observable.change()
// the clean observer is deallocated
observer = nil
// now only the bad observer from earlier is notified. BUT
// the better observers callback is still being called! it never explicitely unsubscribed
// so the observable still has the callback. But its reference to its enclosing self was
// weak, so the class method (shout) is not called.
observable.change()
/* NOW with weak reference AND unsubscribe */
print("")
observer = ConcreteObserverBest()
observer!.watch()
observable.change()
observer = nil
observable.change()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment