Search code examples
swiftgenericsassociated-types

Can I use a generic protocol to describe a delegate in a non generic type?


I'm trying to describe a delegate property with a generic protocol without making the class generic:

protocol ActionHandler: class {
  associatedtype Action
  func handle(_ action: Action)
}

enum SomeAction {
  case dismiss
}

class Foo {
  typealias Action = SomeAction
  // 💣 Protocol 'ActionHandler' can only be used as a generic constraint because it has Self or associated type requirements
  weak var handler: ActionHandler?
  // Ideally some form of ActionHandler<Action>?
}

class Handler: ActionHandler {
  func handle(_ action: SomeAction) {
    switch action {
      case .dismiss:
        print("dismiss")
    }
  }
}

let handler = Handler()
Foo().handler = handler

I can instead replace the property by a closure, but I don't like this pattern as much, and I would have to do so for every method described in the protocol.

enum SomeAction {
  case dismiss
}

class Foo {
  typealias Action = SomeAction
  var handleAction: ((Action) -> Void)?
}

class Handler {
  func handle(_ action: SomeAction) {
    switch action {
      case .dismiss:
        print("dismiss")
    }
  }
}

let handler = Handler()
Foo().handleAction = handler.handle(_:)

Is there a better solution?


Solution

  • You don't have to make Foo generic, but you'll need a very good reason not to. Anyway, here's one way that is built on top of your closure approach - What if we wrap those closures in a type?

    class AnyActionHandler<Action> : ActionHandler {
        var handleClosure: (Action) -> Void
        func handle(_ action: Action) {
            handleClosure(action)
        }
        
        init<ActionHandlerType: ActionHandler>(_ actionHandler: ActionHandlerType) where ActionHandlerType.Action == Action {
            handleClosure = actionHandler.handle(_:)
        }
    }
    

    Now if there are more methods in ActionHandler, you can just add it in this type, rather than adding them everywhere where you assigned to Foo.handler. Foo can just have

    weak var handler: AnyActionHandler<Action>?
    

    And assigning the handler looks like:

    Foo().handler = AnyActionHandler(handler)
    

    This AnyActionHandler type is called a "type eraser".