Last active
December 30, 2018 09:29
-
-
Save hooman/ae592c6e963cac65edea to your computer and use it in GitHub Desktop.
This file contains 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
// Playground - noun: a place where people can play | |
// | |
// Notifications.swift | |
// | |
// Created by Hooman Mehr (hooman@mac.com) on 7/26/14. | |
// Copyright (c) 2014 Hooman Mehr. New BSD License. | |
// | |
// WARNING: This is a sample code to study and to demostrate some Swift capabilities | |
// and limitations. Use it at your own risk. | |
// This source file should work as a module if you place it a framework project. | |
// It should work both on OS X and iOS. The entry points of the module are 'post()' function | |
// 'notificationCenter' singleton and 'ObservationSession' helper class for observers | |
// which automates observer lifecycle and handler typing. The key data object is a struct named | |
// 'Notification'. There is also a couple of optional components, 'PostsNotifications' is a protocol | |
// for publishing posted notifications of a type, and an 'Observable' boxing class that is used to | |
// define observable properties. | |
// | |
// Alternate naming style: | |
// | |
// Notification --> Event | |
// Observer --> Listener | |
// Center --> Manager | |
// | |
// WARNING: Not tested, Not thread-safe, Not scalable, Not async, ... & sequential lookup | |
//// MODULE PROTOCOLS & DATA OBJECTS | |
/// The data object (struct) that is passed between subject (notification publisher) and observer. | |
public struct Notification { | |
public enum Kind: Int { | |
case Generic // Generic notifications (distinguished with name only) | |
case DidSet // Property didSet notifications | |
case WillSet // Property willSet notifications | |
case System // System generated notifications such as low memory, locale change | |
case Cloud // Cloud generated push notifications | |
case Action // User interface action notifications such as button tap. | |
case NotificationCenter // Notifications posted by the notification center itself. | |
// Extendable with more event types. | |
} | |
let kind: Kind = .Generic | |
let name: String = "" // "": anonymous notification (any name in criteria) | |
let senderId: ObjectIdentifier? // nil: anonymous sender (any object in criteria) | |
let senderClass:AnyClass? // nil: anonymous sender class (any class in criteria) | |
let data: Any? // nil: no attached data | |
// All initializer functions are optional to make it easy to build different types of | |
// notifications or observer paterns: | |
init( | |
prototype: Notification? = nil, | |
kind: Kind? = nil, | |
name: String? = nil, | |
sender: AnyObject? = nil, | |
senderId: ObjectIdentifier? = nil, | |
senderClass:AnyClass? = nil, | |
data: Any? = nil ) | |
{ | |
if prototype.hasValue { self = prototype! } | |
if kind.hasValue { self.kind = kind! } | |
if name.hasValue { self.name = name! } | |
if data.hasValue { self.data = data! } | |
if sender.hasValue { | |
self.senderId = ObjectIdentifier(sender!) | |
self.senderClass = sender!.dynamicType | |
} else { | |
if senderId.hasValue { self.senderId = senderId } | |
if senderClass.hasValue { self.senderClass = senderClass } | |
} | |
} | |
} | |
/// Notification utility extensions. | |
public extension Notification { | |
/// Returns a copy of the notification with the specified changes. | |
func with( | |
kind: Kind? = nil, | |
name: String? = nil, | |
sender: AnyObject? = nil, | |
senderId: ObjectIdentifier? = nil, | |
senderClass:AnyClass? = nil, | |
data: Any? = nil ) -> Notification | |
{ | |
return Notification(prototype: self, kind: kind, name: name, sender: sender, senderId: senderId, senderClass: senderClass, data: data) | |
} | |
/// post this notification and return it to possibly chain it with another with() or subscript followed by another post. | |
func post() -> Notification { notificationCenter.post(self); return self } | |
func postWith( | |
kind: Kind? = nil, | |
name: String? = nil, | |
sender: AnyObject? = nil, | |
senderId: ObjectIdentifier? = nil, | |
senderClass:AnyClass? = nil, | |
data: Any? = nil ) -> Notification | |
{ | |
return with(kind: kind, name: name, sender: sender, senderId: senderId, senderClass: senderClass, data: data).post() | |
} | |
func didSet<T> (from oldVal: T, to newVal: T) -> Notification { | |
//FIXME: What if T is a reference type? You need to determine what to do to prevent memory leak, while retaining as long as needed. | |
return postWith(kind: .DidSet, data: (oldVal, newVal)) | |
} | |
/// Returns a copy of the notification with the name specified by subscript. | |
subscript(name: String) -> Notification { get { return with(name: name) } } | |
/// Returns a copy of the notification with the sender (source) object specified by subscript. | |
subscript(sender: AnyObject) -> Notification { get { return with(sender: sender) } } | |
} | |
extension Notification: StringLiteralConvertible { | |
public static func convertFromStringLiteral(value: StringLiteralType) -> Notification { | |
return Notification(name: value) | |
} | |
public static func convertFromExtendedGraphemeClusterLiteral(value: StringLiteralType) -> Notification { | |
return Notification(name: value) | |
} | |
} | |
/// Basic notification center protocol: | |
public protocol NotificationCenter: class, PostsNotifications { | |
func post(Notification) | |
func addObserver(AnyObject, criteria: Notification, handler: (AnyObject, Notification) -> Void) | |
func removeObserver(ObjectIdentifier) | |
var didAddObserver: Notification {get} // notification data is (observer, criteria, handler) | |
var didRemoveObserver: Notification {get} // notification data is observer | |
} | |
/// Optional protocol for a type to reveal its posted notifications: | |
public protocol PostsNotifications { | |
var notifications: [Notification] { get } | |
} | |
public func notificationsOf(observables: PostsNotifications...) -> [Notification] { | |
return observables.map { $0.notifications } .reduce([], + ) | |
} | |
/// Primary interface for observer is this generic helper class. | |
/// Part 1: Main skeleton of observer session helper class. (Internal details) | |
public class ObservationSession<T: AnyObject> { | |
weak var observer: T? | |
let observerId: ObjectIdentifier | |
unowned let center: NotificationCenter | |
var observeCount: Int = 0 | |
required public convenience init (observer: T) { | |
self.init(observer: observer, notificationCenter: notificationCenter) | |
} | |
public init (observer: T, notificationCenter: NotificationCenter) { | |
self.observer = observer | |
self.observerId = ObjectIdentifier(observer) | |
self.center = notificationCenter | |
center.addObserver(self, criteria: center.didAddObserver[observer]) { myself, _ in | |
let me = myself as ObservationSession | |
me.observeCount = me.observeCount + 1 | |
} | |
} | |
deinit { | |
println("deinit: ObservationSession") | |
center.removeObserver(ObjectIdentifier(self)) | |
if observeCount > 0 { center.removeObserver(observerId) } | |
} | |
} | |
/// Primary interface for observer is this generic helper class. | |
/// Part 2: The public interface of session helper class: | |
/// See the last section of the playground version for example usage. | |
public extension ObservationSession { | |
typealias HandlerClosure = (T, Notification) -> Void | |
typealias HandlerMethod = (T) -> (Notification) -> Void | |
func observe (notification:Notification, with handler: HandlerClosure) -> ObservationSession { | |
assert(observer.hasValue, "ObservationSession.observe(): observer deallocated.") | |
center.addObserver(observer!, criteria: notification) { handler($0 as T, $1) } | |
return self | |
} | |
func observe (notification:Notification, with handler: HandlerMethod) -> ObservationSession { | |
assert(observer.hasValue, "ObservationSession.observe(): observer deallocated.") | |
center.addObserver(observer!, criteria: notification) { handler($0 as T)($1) } | |
return self | |
} | |
} | |
/// Generic box to define observable property using our notification module. | |
/// The basic interface for posting notifications is a post() free function. | |
/// This is a utility to help model classes publish observable properties. | |
/// See the last section of the playground version for example usage. | |
public final class Observable<T>: PostsNotifications { | |
public var $: T { didSet { prop.didSet(from: oldValue, to: $) } } | |
private let prop: Notification | |
public var name: String { get { return prop.name } } | |
public var type: Any.Type { get { return prop.data! as Any.Type} } | |
public var notifications: [Notification] { get { return [prop] } } | |
required public init( | |
prototype: Observable<T>? = nil, | |
name: String? = nil, | |
initValue: T? = nil, | |
owner: AnyObject? = nil, | |
ownerClass: AnyClass? = nil) | |
{ | |
if prototype.hasValue { | |
self.prop = Notification(prototype: prototype!.prop, kind: .DidSet, name: name, sender: owner, senderClass: ownerClass, data: T.self) | |
} else { | |
self.prop = Notification(kind: .DidSet, name: name, sender: owner, senderClass: ownerClass, data: T.self) | |
} | |
if initValue.hasValue { | |
self.$ = initValue! | |
} else if prototype.hasValue { | |
self.$ = prototype!.$ | |
} else { | |
fatalError("Observable: Property has no initial value") | |
} | |
} | |
public func with( | |
name: String? = nil, | |
initValue: T? = nil, | |
owner: AnyObject? = nil, | |
ownerClass: AnyClass? = nil) -> Observable<T> | |
{ | |
return Observable(prototype: self, name: name, initValue: initValue, owner: owner, ownerClass: ownerClass) | |
} | |
} | |
//// INTERNAL IMPLEMENTATION | |
//// Skip to next part to see the rest of public interface, then examples. | |
extension Optional: BooleanType { | |
public var boolValue: Bool { | |
get { | |
switch self { | |
case .Some(_): return true | |
case .None: return false | |
} | |
} | |
} | |
} | |
typealias NotificationHandlerClosure = (AnyObject, Notification) -> Void | |
/// NotificationCenter implementation: | |
/// Part 1: Main skeleton of the class. (ivars) | |
class NaïveNotificationCenter { | |
var observations: [Observation] = [ ] | |
var observers: Dictionary<ObjectIdentifier, Weak<AnyObject>> = [ : ] | |
let didAddObserver = Notification(kind: .NotificationCenter, name: "didAddObserver") | |
let didRemoveObserver = Notification(kind: .NotificationCenter, name: "didRemoveObserver") | |
} | |
/// NotificationCenter implementation: | |
/// Part 2.1: NotificationCenter protocol implementation: | |
extension NaïveNotificationCenter: NotificationCenter { | |
var notifications: [Notification] { get { return [didAddObserver, didRemoveObserver] } } | |
func post(notification: Notification) { | |
for observation in observations.filter({ notification ~= $0.criteria }) { | |
if let observer: AnyObject = observers[observation.observerId]?.$? { | |
observation.handler(observer, notification) | |
} | |
} | |
} | |
func addObserver(observer: AnyObject, criteria: Notification, handler: NotificationHandlerClosure) { | |
let id = ObjectIdentifier(observer) | |
didAddObserver.postWith(data: (id, criteria)) | |
let observation = Observation(observer: observer, criteria: criteria, handler: handler) | |
observers[id] = Weak(observer) | |
observations.append(observation) | |
} | |
func removeObserver(observerId: ObjectIdentifier) { | |
var removeCount: Int = 0 | |
didRemoveObserver.postWith(data: observerId) | |
observers[observerId] = nil | |
for (var i = observations.count-1; i >= 0; i--) { | |
if observations[i].observerId == observerId { observations.removeAtIndex(i); removeCount++ } | |
} | |
if removeCount == 0 { fatalError("NotificationCenter.removeObserver: Nothing removed.") } | |
} | |
/// NotificationCenter implementation: | |
/// Part 2.2: Observer registry records kept by NaïveNotificationCenter: | |
struct Observation { | |
let observerId: ObjectIdentifier | |
let criteria: Notification | |
let handler: NotificationHandlerClosure | |
init(observer: AnyObject, criteria: Notification, handler: NotificationHandlerClosure) { | |
self.observerId = ObjectIdentifier(observer) | |
self.criteria = criteria | |
self.handler = handler | |
} | |
} | |
} | |
/// NotificationCenter implementation: | |
/// Part 2.3: Matching posted notification with registered observers: | |
func match (notification: Notification, with pattern: Notification) -> Bool { | |
// Determines who should be notified. | |
return notification.kind == pattern.kind | |
&& ( pattern.name.isEmpty || notification.name == pattern.name ) | |
&& match( notification.senderId, with: pattern.senderId ) | |
&& match( notification.senderClass, with: pattern.senderClass ) | |
} | |
func ~= (lhs: Notification, rhs: Notification) -> Bool { | |
return match(lhs, with: rhs) | |
} | |
func match<T: Equatable> (value: T?, with pattern: T?) -> Bool { | |
// is a match if criteria does not specify any value | |
// or the two values are equal | |
switch (value, pattern) { | |
case let (.Some(value), .Some(pattern)): return value == pattern | |
case ( _ , .None) : return true | |
case (.None, .Some( _ )) : return false | |
} | |
} | |
func match(cls: AnyClass?, with pattern: AnyClass?) -> Bool { | |
// Is a mach is criteria has no class or if classes are identical | |
if !pattern.hasValue { return true } | |
return cls === pattern | |
} | |
/// Generic boxing utility to keep a weak reference in a dictionary or array. | |
class Weak<T: AnyObject> { | |
weak var $: T? | |
required init(_ object: T) {$ = object } | |
} | |
/// Enhaced version of Weak that supports locking (retaining). | |
class LockableWeak<T: AnyObject>: Weak<T> { | |
var locked: Bool { | |
get { return lock$.hasValue } | |
set(lock) { if lock { lock$ = $? } else { lock$ = nil } } | |
} | |
required init(_ object: T) { super.init(object) } | |
private var lock$: T? | |
} | |
//// PUBLIC INTERFACE, CONTINUED: | |
//// MODULE PRIMARY SINGLETON AND FREE FUNCTION | |
/// This singleton is rarely used, unless you want to add other convenience API: | |
public let notificationCenter: NotificationCenter = NaïveNotificationCenter() | |
/// This free function is the basic interface for observable objects and notification publishers: | |
public func post(notification:Notification) { | |
notificationCenter.post(notification) | |
} | |
/// END OF NOTIFICATIONS MODULE | |
/// PLAYGROUND VERSION ONLY: | |
/// BASIC USAGE DEMO | |
/// General utility base class to support id --> obj conversion. | |
public class Object { | |
internal struct S { | |
static var objects = Dictionary<ObjectIdentifier,Weak<Object>>() | |
} | |
final class func downcast<T: Object>(object: Object) -> T? { return object as? T } | |
final public class func fromId(id:ObjectIdentifier) -> /*Self*/ Object? { | |
if let obj = S.objects[id]?.$ { return /*downcast(obj)*/ obj } else { return nil } | |
} | |
final public class func fromId(id:ObjectIdentifier?) -> /*Self*/ Object? { | |
return id.hasValue ? fromId(id!) : nil | |
} | |
public func toId() -> ObjectIdentifier { return ObjectIdentifier(self) } | |
init() { S.objects[toId()] = Weak(self) } | |
deinit { S.objects[toId()] = nil } | |
} | |
/// A pure-Swift plain model class. This is the model layer. | |
public class Model: Object, Printable, DebugPrintable { | |
public let name: String | |
public var i:Int = 0 | |
public var d:Double = 0 | |
public var s:String = "" | |
public init(name: String) { | |
self.name = name | |
super.init() | |
} | |
public var description: String { return "Model(\(name))" } | |
public var debugDescription: String { get { return description } } | |
} | |
/// The observable subclass of the above. This is model adapter is part of the control layer. | |
class ObservableModel: Model, PostsNotifications { | |
override var i: Int { didSet { didSet("i", oldValue, i) } } | |
override var d: Double { didSet { didSet("d", oldValue, d) } } | |
override var s: String { didSet { didSet("s", oldValue, s) } } | |
var notifications: [Notification] { | |
get { | |
let didSet = Notification(kind: .DidSet, sender: self) | |
return | |
[ didSet["i"].with(data: i.dynamicType), // data is ignored for matching | |
didSet["d"].with(data: d.dynamicType), // but it provides type info | |
didSet["s"].with(data: s.dynamicType) ] // for the client observer | |
} | |
} | |
func didSet<T>(propName: String, _ oldVal: T, _ newVal: T) { | |
Notification(name: propName, sender: self).didSet(from: oldVal, to: newVal) | |
} | |
deinit { println("deinit: \(self)") } | |
} | |
/// Alternate method: Use boxing to make observable properties from the begining. | |
class ObservableModel2: Object, Printable, DebugPrintable, PostsNotifications { | |
let name: String | |
// If binding to class is sufficient you can fully initialize here | |
// by adding ownerClass: ObservableModel2.self parameter, below: | |
let i = Observable(name: "i", initValue: 0) | |
let d = Observable(name: "d", initValue: 0 as Double) | |
let s = Observable(name: "s", initValue: "") | |
var notifications: [Notification] { | |
get { return notificationsOf(i, d, s) } | |
} | |
var description: String { get { return "ObservableModel2(\"\(name)\")" } } | |
var debugDescription: String { get { return description } } | |
init(name: String) { | |
// Phase 1: | |
self.name = name | |
super.init() | |
// Phase 2: | |
// If you need to bind to object, clone it with self here: | |
i = i.with(owner: self) | |
d = d.with(owner: self) | |
s = s.with(owner: self) | |
} | |
deinit { println("deinit: ObservableModel2") } | |
} | |
/// A simple pure-Swift observer class. This is the brain of the control layer. | |
class Controller { | |
// Controller (observer) knows ObservableModel (the subject of observation) | |
// Actually, ObservableModel is part of the control layer. Like early Java | |
// days, we can write a code generator to automatically generate Observable | |
// subclass and handleDidSet skeleton. I hope we get something better soon. | |
let session: ObservationSession<Controller>! | |
init() { | |
let modelDidSet = Notification(kind: .DidSet, senderClass: ObservableModel.self) | |
let model2DidSet = modelDidSet.with(senderClass: ObservableModel2.self) | |
session = ObservationSession(observer: self) | |
.observe ( modelDidSet ) { | |
print("* "); $0.handleDidSet($1) | |
} | |
.observe ( model2DidSet, with: Controller.handleDidSet ) | |
} | |
func handleDidSet(notification:Notification) { | |
typealias Ints = (Int, Int) | |
typealias Doubles = (Double, Double) | |
typealias Strings = (String, String) | |
// If you comment out this if, everything else seems to work: | |
if let obj = Object.fromId(notification.senderId) { | |
switch obj { | |
case let model as ObservableModel: print("From \(model.description).") //COMPILER_BUG: Runtime error: EXC_BAD_ACCESS | |
case let model as ObservableModel2: print("From \(model.description)).") | |
default: fatalError("Notification from an unexpected sender") | |
} | |
} | |
switch (notification.name, notification.data!) { | |
case let ("i", value as Ints): println("i: \(value.0) -> \(value.1).") | |
case let ("d", value as Doubles): println("d: \(value.0) -> \(value.1).") | |
case let ("s", value as Strings): println("s: \"\(value.0)\" -> \"\(value.1)\".") | |
default: fatalError("Observer.handleSubjectUpdate: Unexpected .DidSet notification") | |
} | |
} | |
deinit { println("deinit: Controller") } | |
} | |
// Now lets play with this model/controller a little bit: | |
println("Begin") | |
var s1 = ObservableModel(name: "Bob") | |
var s2 = ObservableModel2(name: "Jack") | |
var o: Controller! = Controller() | |
s1.i = 6 | |
s1.d = 3.14 | |
s1.s = "bearded" | |
s2.i.$ = 7 | |
s2.d.$ = 3.1416 | |
s2.s.$ = "mustache" | |
s1.i = 19 | |
s1.d = 33964.147 | |
s1.s = "shaven" | |
s2.i.$ = 921 | |
s2.d.$ = 8678.456 | |
s2.s.$ = "hippie" | |
o = nil // This does not cause deinit in playground, since playground keeps the object alive, but works elsewhere. | |
println("End") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment