Skip to content

Instantly share code, notes, and snippets.

@KingOfBrian
Last active February 4, 2022 11:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KingOfBrian/a5dfb53637dacbad98baf6b310ba199b to your computer and use it in GitHub Desktop.
Save KingOfBrian/a5dfb53637dacbad98baf6b310ba199b to your computer and use it in GitHub Desktop.
PublicationCenter
//
// PublicationCenter.swift
//
// Created by Brian King on 5/17/16.
// Copyright © 2016 Raizlabs. All rights reserved.
//
import Foundation
/// PublicationCenter is an NSNotificationCenter style object that
/// uses protocols and blocks instead of Notification objects. The PublicationCenter
/// API has two sides. The Publisher side, which dispatches blocks to update a specific
/// Protocol, and the Subscription side, where published updates are invoked on all
/// Protocols the subscriber conforms to. PublicationCenter uses Generics to pass
/// updates for a given Protocol to any subscribers that conform to that protocol.
///
/// The PublicationCenter also has the ability to retain the last block published and
/// invoke that block on all new subscribers that conform to the protocol. This can be
/// used to inject state as well as observe for new events as they happen. To use this
/// API, publish updates using the Publisher API.
///
/// The PublicationCenter only stores weak references to the subscribers and the Publisher objects.
/// Being added as a subscriber will not retain the object, and if the publisher object is released
/// The last values stored will not be updated. The last block published is strongly retained
/// by the Publisher object and care should be taken here to avoid retain loops.
///
/// For an example of a few usage patterns, see PublicationCenterTests.swift
///
/// Currently Thread Safety is provided by Thread Confinement and is the consumers responsibility.
public class PublicationCenter {
public static let shared = PublicationCenter()
private var subscriptions = Array<Weak<AnyObject>>()
private var publishers = Array<Weak<Publisher>>()
public init() {}
// Add the object as a subscriber to receive updates sent to this broadcaster. The subscriber is not
// strongly retained and does not need to be removed from the broadcaster.
public func addSubscriber(subscriber: AnyObject) {
if subscriptions.indexOf({ $0.object === subscriber }) == nil {
subscriptions.append(Weak(object: subscriber))
for publisher in publishers {
for registration in publisher.object?.registrations ?? [] {
registration.subscribeBlock(subscriber)
}
}
}
subscriptions = subscriptions.filter() { $0.object != nil }
}
// Remove the object from the PublicationCenter so the object no longer receives published updates.
public func removeSubscriber(subscriber: AnyObject) {
if let index = subscriptions.indexOf({ $0.object === subscriber }) {
subscriptions.removeAtIndex(index)
}
subscriptions = subscriptions.filter() { $0.object != nil }
}
/// Invoke the block with every subscriber in the PublicationCenter. This method bypasses
/// the publisher and the block will not be retained.
///
/// - parameter typeHint: A type hint. This can be specified so that the type is not needed in the parameter list of the block. If the type is specified in the parameter list, there is no need to specify this parameter.
/// - parameter block: The block to invoke
public func publish<P>(typeHint: P.Type = P.self, block: (P) -> Void) {
for subscription in subscriptions {
if let object = subscription.object as? P {
block(object)
}
}
}
/// Return a new publisher object that will push new values to the subscribers and
/// retain the last published block.
/// - returns: A new Publisher object
public func publisher() -> Publisher {
let publisher = Publisher(publicationCenter: self)
publishers.append(Weak(object: publisher))
publishers = publishers.filter() { $0.object != nil }
return publisher
}
}
/// The Publisher represents a source of information publishing updates. The
/// publisher is responsible for the last published update that future subscribers
/// can use for the initial state.
///
/// If storage of the last update is not desired, call publish on the PublicationCenter
/// or just invoke publish without retaining the publisher object.
public class Publisher {
private var registrations = Array<Registration>()
private let publicationCenter: PublicationCenter
private init(publicationCenter: PublicationCenter) {
self.publicationCenter = publicationCenter
}
}
extension Publisher {
/// Invoke the block with every subscriber in the publicationCenter.
/// The block will be retained until the next update of type P, or until clear is called.
///
/// - parameter typeHint: A type hint. This can be specified so that the type is not needed in the parameter list of the block. If the type is specified in the parameter list, there is no need to specify this parameter.
/// - parameter block: The block to invoke
public func publish<P>(typeHint: P.Type = P.self, block: (P) -> Void) {
let index = indexForRegistration(P.self)
registrations[index].lastPublishedBlock = block
for subscription in publicationCenter.subscriptions {
if let object = subscription.object as? P {
block(object)
}
}
}
// Clear out the last published block from the Publisher.
public func clear<P>(type: P.Type) {
let index = indexForRegistration(P.self)
registrations[index].lastPublishedBlock = nil
}
}
extension Publisher {
// The Registration struct is an internal Type-erasure container. It is useless
// without the types injected by the Generic methods. I would be interested in
// what a Generic Registration struct would look like, but limitations of
// Swift 2 made this approach appear the most viable.
private struct Registration {
let type: Any.Type
let subscribeBlock: (AnyObject) -> Void
var lastPublishedBlock: Any?
}
/// This method is not private because it is used in the unit tests.
/// - parameter object: The object to trigger with the last block
func triggerLastPublishedBlock<P>(object: P) {
let index = indexForRegistration(P.self)
if let lastValueBlock = registrations[index].lastPublishedBlock {
guard let typedValueBlock = lastValueBlock as? P -> Void else {
fatalError("Invalid Registration")
}
typedValueBlock(object)
}
}
/// Return the index of the registration for the specified type in the publisher.
/// If the registration does not exist, create one.
/// - parameter type: The type for the registration
/// - returns: The index of the registration
private func indexForRegistration<P>(type: P.Type) -> Int {
if let index = registrations.indexOf({ $0.type == P.self }) {
return index
}
else {
let subscribeBlock: (AnyObject) -> Void = { [weak self] subscriber in
if let subscriber = subscriber as? P {
self?.triggerLastPublishedBlock(subscriber)
}
}
let registration = Registration(type: P.self, subscribeBlock: subscribeBlock, lastPublishedBlock: nil)
registrations.append(registration)
return registrations.count - 1
}
}
}
/// Internal weak container
private struct Weak<T: AnyObject> {
weak var object: T?
}
//
// PublicationCenterTests.swift
//
// Created by Brian King on 5/17/16.
// Copyright © 2016 Raizlabs. All rights reserved.
//
import XCTest
@testable import Utility
protocol FooType {
var value: Int { get }
func method()
}
protocol BarType {
func otherMethod(value: Int)
}
class TestFooBar: FooType, BarType {
var value: Int = 0
var allValues: [Int] = Array()
func method() {
value = 1
}
func otherMethod(value: Int) {
self.value = value
allValues.append(value)
}
}
class PublicationCenterTest: XCTestCase {
let publicationCenter = PublicationCenter()
lazy var publisher: Publisher = {
return self.publicationCenter.publisher()
}()
let t1 = TestFooBar()
override func setUp() {
super.setUp()
}
func testTrigger() {
publicationCenter.addSubscriber(t1)
publisher.triggerLastPublishedBlock(t1 as FooType)
XCTAssert(t1.value == 0)
publisher.triggerLastPublishedBlock(t1 as BarType)
XCTAssert(t1.value == 0)
publisher.publish(FooType.self) { foo in
foo.method()
}
XCTAssert(t1.value == 1)
publisher.publish(BarType.self) { bar in
bar.otherMethod(3)
}
XCTAssert(t1.value == 3)
publisher.publish(BarType.self) { bar in
bar.otherMethod(4)
}
XCTAssert(t1.value == 4)
}
func testSubscribeTriggersInitialValue() {
publisher.publish(FooType.self) { foo in
foo.method()
}
publisher.publish(BarType.self) { bar in
bar.otherMethod(4)
}
publicationCenter.addSubscriber(t1)
XCTAssert(t1.value == 4)
publisher.triggerLastPublishedBlock(t1 as FooType)
XCTAssert(t1.value == 1)
}
func testClearLastValue() {
publisher.clear(BarType) // This should no-op
publisher.publish() { (bar: BarType) in
bar.otherMethod(4)
}
publisher.clear(BarType)
publicationCenter.addSubscriber(t1)
XCTAssert(t1.value == 0)
}
func testTriggerUnsubscribed() {
publisher.triggerLastPublishedBlock(t1 as BarType)
XCTAssert(t1.value == 0)
publisher.triggerLastPublishedBlock(t1 as FooType)
XCTAssert(t1.value == 0)
publisher.publish(BarType.self) { bar in
bar.otherMethod(9)
}
XCTAssert(t1.value == 0)
publisher.triggerLastPublishedBlock(t1 as BarType)
XCTAssert(t1.value == 9)
}
func testTriggerMultiplePublishers() {
let otherPublisher = publicationCenter.publisher()
otherPublisher.publish(BarType.self) { bar in
bar.otherMethod(8)
}
publisher.publish(BarType.self) { bar in
bar.otherMethod(9)
}
publicationCenter.addSubscriber(t1)
XCTAssert(t1.allValues == [8, 9])
publisher.publish(BarType.self) { bar in
bar.otherMethod(10)
}
XCTAssert(t1.allValues == [8, 9, 10])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment