Skip to content

Instantly share code, notes, and snippets.

@rcdilorenzo
Created November 28, 2016 12:04
Show Gist options
  • Save rcdilorenzo/8880049ebe4bda24eac60cc8c87333e0 to your computer and use it in GitHub Desktop.
Save rcdilorenzo/8880049ebe4bda24eac60cc8c87333e0 to your computer and use it in GitHub Desktop.
TIL: Generic Protocol Limitations in Swift

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)
                }
            }
        })
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment