0

I am developing a state management library. The original design only has 1 listener, which works great until I need to support multiple listeners.

The original design is here: Swift how to use generic protocol in generic class

This is what I have done to support multiple listeners:

public protocol StateObserver: AnyObject {
  associatedtype State
  func didUpdateState(_ state: State)
}

public final class StateStore<Observer: StateObserver> {

  struct WeakRef<T: AnyObject> {
    weak var value: T?
  }

  public private(set) var state: Observer.State
  private var observers = [WeakRef<Observer>]()
  
  public init(initialState: Observer.State) {
    state = initialState
  }
  
  public func addObservers(_ observers: [Observer]) {
    self.observers += observers.map { WeakRef(value: $0) }
  }
  
  public func update(_ block: (inout Observer.State) -> Void) {
    var nextState = state
    block(&nextState)
    state = nextState
    notify()
  }
  
  public func notify() {
    for observer in observers {
      observer.value?.didUpdateState(state)
    }
  }
}

Now I need to create the store with 2 observers:

class MyScene: SKScene {
  init {
    let leftPanel = LeftPanelSKNode()
    let topBar = TopBarSKNode()
    let store: StateStore<?> // How to make this support `LeftPanelSKNode `, `TopBarSKNode`, and `MyScene`? 
    store.addObservers([leftPanel, topBar, self])
  }

Now I am stuck here. I need to create a StateStore<?> of something, which can be either MyScene, LeftPanelSKNode and TopBarSKNode.

1 Answers1

1

First of all, I have to say that what you are building already exists in many reactive libraries:

CurrentValueSubject in Apple's Combine;

BehaviorSubject in RxSwift;

You can also check the small internal class I've made myself, it allows to hold the state and observe it ObservableProperty.

Back to your question, I've found a way to add the StateObserver one by one while keeping only the weak reference to them.

public protocol StateObserver: AnyObject {
  associatedtype State
  func didUpdateState(_ state: State)
}

class Node1: StateObserver {
    typealias State = Int

    func didUpdateState(_ state: Int) { }
}

class Node2: StateObserver {
    typealias State = Int

    func didUpdateState(_ state: Int) { }
}

class StateStore<StateType> {
    private(set) var state: StateType

    init(_ initialState: StateType) {
        self.state = initialState
    }

    private var observers: [(StateType) -> Void] = []

    func observe<Observer: StateObserver>(by observer: Observer) where Observer.State == StateType {
        weak var weakObserver = observer

        observers.append { state in
            weakObserver?.didUpdateState(state)
        }
    }

    func notify() {
        observers.forEach {
            $0(self.state)
        }
    }
}

let store = StateStore<Int>(0)

let node1 = Node1()
let node2 = Node2()

store.observe(by: node1)
store.observe(by: node2)

Adding the array-based observe API might be a problem because of the associatedtype in the StateObserver.

  • is it possible not to use closure? –  Apr 25 '21 at 19:33
  • so the internal observers list inside the store has to be closure because of swift's limitation of generic protocol, am i right? –  Apr 26 '21 at 21:53
  • yes, you can not just use such a protocol as a type of property or a parameter. You can play with it a little more using a technique called Type Erasure and maybe get something that suits you more than what I've suggested. – Michael Krutoyarskiy Apr 27 '21 at 06:21