Skip to content

Instantly share code, notes, and snippets.

@rnapier
Last active April 17, 2022 17:51
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 rnapier/90072db9e741a181eb9eb0481dde08c5 to your computer and use it in GitHub Desktop.
Save rnapier/90072db9e741a181eb9eb0481dde08c5 to your computer and use it in GitHub Desktop.
New ideas on Observer pattern in Swift
import UIKit
// Lots of ideas here that I'd love thoughts about. Some of this creates new highly generic types
// (UniqueIdentifier, IdentifiedSet) that may have broader purpose. Some of it defines a new Observer pattern.
// I've been playing around with this UniqueIdentifier type. Its purpose is to let you store things
// in dictionaries (or possibly sets) that you couldn't otherwise.
// It's just a self-referencial ObjectIdentifier. I used to use NSUUID for this purpose, but wondering
// if this is better.
final class UniqueIdentifier: Hashable, Equatable {
private lazy var _identifier: ObjectIdentifier = ObjectIdentifier(self)
var hashValue: Int { return _identifier.hashValue }
}
func == (lhs: UniqueIdentifier, rhs: UniqueIdentifier) -> Bool {
return lhs._identifier == rhs._identifier
}
//
//
//
// I'm using it in this IdentifiedSet experiment. An IdentifiedSet is a set of anything (not just
// Hashables). When items are inserted, an identifier is returned that can be used to remove it.
// NOTE: @connerk points out that this isn't very "set-like." A better name may be "Bag." https://en.wikipedia.org/wiki/Multiset
struct IdentifiedSet<Element> {
private var _elements: [UniqueIdentifier: Element] = [:]
// Adds an item, returning an identifier
mutating func insert(_ element: Element) -> UniqueIdentifier {
let identifier = UniqueIdentifier()
_elements[identifier] = element
return identifier
}
// Removes an item, based on its identifier
mutating func remove(_ identifier: UniqueIdentifier) {
_elements[identifier] = nil
}
}
// It's not hard to make IdentifiedSet a Collection; see below. Just didn't want to mix it in here because I don't need it.
extension IdentifiedSet: Sequence {
func makeIterator() -> AnyIterator<Element> {
return AnyIterator(_elements.values.makeIterator())
}
}
//
//
//
// What good is this? Here's an very simple observer pattern using it.
// First, we define a NotificationHandler
protocol NotificationHandler {
associatedtype Notification
func notify(_ notification: Notification)
}
// And we wrap IdentifiedSet in a simple Observers type
struct Observers<Handler: NotificationHandler> {
private var _observers = IdentifiedSet<Handler>()
// "add" matches other common Observer patterns better than "insert"
// The use of a set is an implementation detail
mutating func add(_ observer: Handler) -> UniqueIdentifier {
return _observers.insert(observer)
}
// remove is very often called with an Optional, so it's nice to accept it
mutating func remove(_ identifier: UniqueIdentifier?) {
if let identifier = identifier {
_observers.remove(identifier)
}
}
func notify(_ notification: Handler.Notification) {
for observer in _observers {
observer.notify(notification)
}
}
}
//
//
//
// Here's a simple observer. It's a struct of closures.
struct LoginHandler {
var didLogin: () -> Void
var didLogout: () -> Void
}
// And it's a NotificationHandler. In practice I expect this often to be a switch on an enum.
extension LoginHandler: NotificationHandler {
func notify(_ isLoggedIn: Bool) {
if isLoggedIn {
didLogin()
} else {
didLogout()
}
}
}
// And we have a class that is observable
final class LoginManager {
static let sharedManager = LoginManager()
var isLoggedIn = false {
didSet {
observers.notify(isLoggedIn) // I like how simple, yet explicit, this winds up being.
}
}
// Adding and removing observers is very easy. Just obj.observers.add(...), obj.observers.remove(...).
// No wrapper functions are needed.
// It's also very scalable. You could have obj.somethingObservers and obj.somethingElseObservers trivially.
// The one danger I see is that it's possible for a caller to completely replace the observers collection.
// That danger, if it really is one, could be fixed by making Observers a class and 'let' here.
// Of course it's also possible for a caller to notify the observers, which would be odd, but not really a "problem."
var observers = Observers<LoginHandler>()
func logout() {
isLoggedIn = false
}
func login() {
isLoggedIn = true
}
}
// And we have an observer
class LoginViewController {
weak var messageLabel: UILabel! = nil // Just for demonstration
var loginManager: LoginManager { return LoginManager.sharedManager }
var loginObserver: UniqueIdentifier? = nil
// Start observing
func viewDidAppear(_ animated: Bool) {
let messageLabel = self.messageLabel! // no self, no headaches
loginObserver = loginManager.observers.add(
LoginHandler(
didLogin: { messageLabel.text = "We are logged in" },
didLogout: { messageLabel.text = "We are NOT logged in" }))
}
// Stop observing
func viewDidDisappear(_ animated: Bool) {
loginManager.observers.remove(loginObserver) // This is that Optional case I was talking about
}
@IBAction func login(_ sender: AnyObject) {
loginManager.login()
}
}
// In practice, extensions can make this even nicer. For instance, if you don't want to implement
// separate didLogin/didLogout methods, create a new init()
extension LoginHandler {
init(didChangeLoginState: (Bool) -> Void) {
didLogin = { didChangeLoginState(true) }
didLogout = { didChangeLoginState(false) }
}
}
// Then you can have code like:
//override func viewDidAppear(_ animated: Bool) {
// loginObserver = loginManager.observers.add(LoginHandler(didChangeLoginState: updateMessage))
//}
//
//func updateMessage(loggedIn: Bool) {
// messageLabel.text = "We are\(loggedIn ? "" : "NOT") logged in"
//}
// The power of this approach is that you can offer many equivalent ways of observing, and the caller
// can easily create new ways of observing by creating new init's on the handler. You could easily
// offer a delegate interface with init(delegate: DelegateProtocol), or provide default {} handlers to
// allow optional protocols.
//
// Other stuff
//
// See also https://gist.github.com/rnapier/e973e0821c5bb0aa863c for an older and I think less powerful version of this approach.
// I think this is the correct way to make IdentifiedSet into a Collection. Is there a better approach these days?
struct IdentifiedSetIndex<Element> {
private var _index: DictionaryIndex<UniqueIdentifier, Element>
}
extension IdentifiedSetIndex: Comparable {}
func < <Element>(lhs: IdentifiedSetIndex<Element>, rhs: IdentifiedSetIndex<Element>) -> Bool {
return lhs._index < rhs._index
}
func == <Element>(lhs: IdentifiedSetIndex<Element>, rhs: IdentifiedSetIndex<Element>) -> Bool {
return lhs._index == rhs._index
}
extension IdentifiedSet: Collection {
typealias Index = IdentifiedSetIndex<Element>
var startIndex: Index { return Index(_index: _elements.startIndex) }
var endIndex: Index { return Index(_index: _elements.endIndex) }
subscript (position: Index) -> Element {
return _elements.values[position._index]
}
func index(after i: Index) -> Index {
return IdentifiedSetIndex(_index: _elements.index(after: i._index))
}
}
@followben
Copy link

Agreed... IdentifiedSet is a bit confusing. Just a dictionary where the keys are determined for you, right? I had to lookup Bag/ Multiset, but it sounds like the correct terminology.

I like where this ends up, but stumbling on the intermediary nature of LoginHandler. Could this simply be a closure, or self assuming I conform to a protocol?

I'm gonna abdicate on collection conformance, as it's hurting my brain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment