Skip to content

Instantly share code, notes, and snippets.

@hooman
Last active December 30, 2018 09:29
Show Gist options
  • Save hooman/ae592c6e963cac65edea to your computer and use it in GitHub Desktop.
Save hooman/ae592c6e963cac65edea to your computer and use it in GitHub Desktop.
// 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