TIL: Apparently, Swift does not support creating concrete protocols from generic ones since it uses associated types for more customizable generic protocol adaptation. This is in contrast to classes and structs. Here is an example of a generic struct from my situation:
fileprivate struct WindowScopeUpdate<State> {
let update: (State) -> (State)
}
An example usage would be like this (where TaskListWindowState
is a concrete type such as a class, struct, or non-generic protocol):
// ...
var taskUpdates = [WindowScopeUpdate<TaskListWindowState>]()
// ...
Contrast this behavior with that of a generic protocol:
protocol WindowScopeWatcher {
associatedtype State
// ...
func windowScopeShouldUpdate(_ old: State, new: State) -> Bool
func windowScopeDidUpdate(_ old: State, new: State)
}
To use this type of generic protocol:
func notifyWatcher<T: WindowScopeWatcher>(_ watcher: WindowScopeWatcher) where T.State == TaskListWindowState {
// ...
}
Unfortunately, then, this is a specific type of WindowScopeWatcher
and cannot be used to the type of a return object. It must instead be an object that defines inside of it a typealias State = _
. This caused problem in my particular scenario since I wanted to have multiple watchers for a concrete scope type (e.g. WindowScope<TaskListWindowState, MyControllerThatIsAWindowScopeWatcher>
). I could not do this since when it pulled out the scope it would have to then specify the controller that is watching (e.g. MyControllerThatIsAWindowScopeWatcher
) and no other controller could resolve that type.
To mitigate this issue, I ended up using a non-generic protocol and then coercing it from the protcol type to the specific state (e.g. windowState as! TaskListWindowState
). Below is the full source of the generic scope and state objects (notice the lines with comments on them):
//
// WindowScope.swift
//
// Created by Christian Di Lorenzo on 11/26/16.
// Copyright © 2016 Light Design. All rights reserved.
//
import Foundation
import Cocoa
protocol WindowScopeWatcher {
var scopeID: String { get set }
func isEqual(_ object: Any?) -> Bool
func windowScopeShouldUpdate(_ old: WindowState, new: WindowState) -> Bool
func windowScopeDidUpdate(_ old: WindowState, new: WindowState)
}
fileprivate struct WindowScopeUpdate<State> {
let update: (State) -> (State)
}
fileprivate class WindowScopeRepository {
static let shared = WindowScopeRepository()
var scopes = [String: Any]()
func shared<T>(id: String) -> T {
return scopes[id] as! T
}
func setShared(id: String, scope: Any) {
scopes[id] = scope
}
}
class WindowScope<State: WindowState>: NSObject, AppScopeWatcher {
fileprivate var state = State()
fileprivate var watchers = [WindowScopeWatcher]()
fileprivate var isUpdating = false
fileprivate var pendingUpdates = [WindowScopeUpdate<State>]()
static func shared(id: String) -> WindowScope<State> {
return WindowScopeRepository.shared.shared(id: id)
}
static func setShared(id: String, scope: WindowScope<State>) {
WindowScopeRepository.shared.setShared(id: id, scope: scope)
}
static func newShared(id: String) -> Self {
let scope = self.init()
FMAppScope.shared.registerWatcher(scope as! AppScopeWatcher)
setShared(id: id, scope: scope)
return scope
}
override required init() {
}
func currentState() -> State {
return state
}
func updateState(_ update: @escaping (State) -> (State)) {
if isUpdating {
pendingUpdates.append(WindowScopeUpdate(update: update))
} else {
isUpdating = true
performUpdateState(update)
isUpdating = false
}
}
func notifyWatcher(_ watcher: WindowScopeWatcher) {
state.notify(completion: { newState in
self.state = newState as! State // <================== Note the type coercion on this line
watcher.windowScopeDidUpdate(newState, new: newState)
})
}
func registerWatcher(_ watcher: WindowScopeWatcher) {
if !watchers.contains(where: { watcher.isEqual($0) }) {
watchers.append(watcher)
}
}
func deregisterWatcher(_ watcher: WindowScopeWatcher) {
watchers = watchers.filter({ !watcher.isEqual($0) })
}
func shouldUpdate(_ old: AppState, new: AppState) -> Bool {
return true
}
func didUpdate(_ old: AppState, new: AppState) {
notifyWatchers(state, new: state)
}
fileprivate func performUpdateState(_ update: (State) -> (State)) {
let oldState = state
state = update(oldState.copy() as! State) // <============= Note the type coercion on this line
notifyWatchers(oldState, new: state)
if let nextUpdate = pendingUpdates.popLast() {
performUpdateState(nextUpdate.update)
}
}
fileprivate func notifyWatchers(_ old: State, new: State) {
new.notify(completion: { newState in
self.state = newState as! State // <=================== Note the type coercion on this line
for watcher in self.watchers {
if watcher.windowScopeShouldUpdate(old, new: newState as! State) {
watcher.windowScopeDidUpdate(old, new: newState as! State)
}
}
})
}
}