Search code examples
swiftprotocolsassociated-types

How do I make an Observer pattern with 2 Swift protocols, where the two associatedtypes must be the same?


I'm trying to use SE-0142 (Associated Type Constraints) to make an Observer pattern with 2 protocols, IsObserver (like a client) and HasObservers (like a server), where there's a shared DataType that represents the type of the thing being observed.

I need objects conforming to HasObservers to be capable of being a struct or a class, and I want the IsObserver to be intentionally limited to be a class (want, but do not need).

I'm not good with generics... after several hours I got this far, with the compiler error in a comment inline below. I'm stuck and not sure where to go next, and I'm not sure this approach is even possible or reasonable. All help much appreciated!

import Foundation

protocol IsObserver: class {
    associatedtype DataType
    func dataDidUpdate(_ data: [DataType])
}

struct Observation<T: IsObserver> {
    weak var observer: T?
}


protocol HasObservers {
    associatedtype DataType : IsObserver where DataType.DataType == DataType
    static var observations: [ObjectIdentifier : Observation<IsObserver>] { get set } // ERROR: "Value of protocol type 'IsObserver' cannot conform to 'IsObserver'; only struct/enum/class types can conform to protocols"
    static func tellObserversDataDidUpdate(_ data: [DataType])
}

extension HasObservers {
    static func tellObserversDataDidUpdate(_ data: [DataType]) {
        for (id, observation) in observations {
            guard let observer = observation.observer else {
                observations.removeValue(forKey: id)
                continue
            }
            observer.dataDidUpdate(data)
        }
    }

    static func addObserver<T: IsObserver>(_ observer: T) {
        let id = ObjectIdentifier(observer)
        let ob = Observation.init(observer: observer)
        observations[id] = ob
    }

    static func removeObserver<T: IsObserver>(_ observer: T) {
        let id = ObjectIdentifier(observer)
        observations.removeValue(forKey: id)
    }
}

UPDATE: Alright, got there in the end. Was harder than I thought and required type erasure. In this gist there are two versions: the first one is the one with associatedtype protocols per the original question. It is limited though - the object that is the observer can only observe one type. So I made another variant that can have multiple types but doesn't use associatetype protocols so the observer has to check the type manually.

https://gist.github.com/xaphod/4f8a6402429759b6b3fd8ea2d8ea53c4


Solution

  • I'll simplify your use case a bit (ignore observations) to hopefully get the concept across.

    HasObservers basically has 2 associated types - the DataType and the IsObserver type, and then you'd constrain the IsObserver type to have the right DataType

    protocol IsObserver {
      associatedtype DataType
      func dataDidUpdate(_ data: [DataType])
    }
    
    protocol HasObservers {
      associatedtype DataType
      associatedtype ObserverType: IsObserver where ObserverType.DataType == DataType
    
      static func addObserver(_ observer: ObserverType)
      static func tellObserversDataDidUpdate(_ data: [DataType])
    
      // ..
    }