Search code examples
iosswiftgenericsprotocolsswift-protocols

Swift: conformance to the Protocol with Generic method with "where" clause


Summary:

I'd like to create a Class<T> which would have a corresponding ClassDelegate protocol with func<T> in it.

Goal:

To reuse a single object and behavior with multiple object classes. Receive a delegate callback with already a specialized class, without the need to cast the object to a specific class to work with it.

Sample Code:

A protocol with a generic method:

protocol GenericTableControllerDelegate: AnyObject {
    func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}

A generic base UITableViewController subclass:

open class GenericTableController<DataType>: UITableViewController {
    weak var delegate: GenericTableControllerDelegate?
    var data = [DataType]()

    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = data[indexPath.row]
        delegate?.controller(controller: self, didSelect: item)
    }
}

A specialized version of the GenericTableController:

final class SpecializedTableController: GenericTableController<NSObject> {}

Client of the SpecializedTableController - achieves the result, but requires type-casting to be able to access the specialized data type:

final class ClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) {
        if let value = value as? NSObject {
            // Requires unwrapping and casting
        }
    }
}

Client of the SpecializedTableController, with "where" requirement - the only problem that it doesn't compile

final class AnotherClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) where T: NSObject {
        // `value` is String
    }    
}

Type 'AnotherClientOfTableController' does not conform to protocol 'GenericTableControllerDelegate' Do you want to add protocol stubs?

Is there a way to have a protocol with a generic method and being able to have a concrete (specialized) type in that method implementation?

Are there close alternatives achieving satisfying similar requirement (having a generic class but being able to handle a concrete type in the delegate callback)?

screenshot


Solution

  • Your mistake is in the protocol:

    protocol GenericTableControllerDelegate: AnyObject {
        func controller<T>(controller: GenericTableController<T>, didSelect value: T)
    }
    

    This says that in order to be a GTCD, a type must accept any type T passed to this function. But you don't mean that. You meant this:

    public protocol GenericTableControllerDelegate: AnyObject {
        associatedtype DataType
        func controller(controller: GenericTableController<DataType>, didSelect value: DataType)
    }
    

    And then you wanted the delegate's DataType to match the table view's DataType. And that gets us into the world of PATs (protocols with associated types), type erasers, and generalized existentials (which don't exist yet in Swift), and really it just gets to be a mess.

    While this is a use case that generalized existentials are particularly well suited for (if they're ever added to Swift), in a lot of cases you probably don't want this anyway. The delegation pattern is an ObjC pattern developed before the addition of closures. It used to be very hard to pass functions around in ObjC, so even very simple callbacks were turned into delegates. In most cases, I think Richard Topchiy's approach is exactly right. Just pass a function.

    But what if you really want to keep the delegate style? We can (almost) do that. The one glitch is that you can't have a property called delegate. You can set it, but you can't fetch it.

    open class GenericTableController<DataType>: UITableViewController
    {
        // This is the function to actually call
        private var didSelect: ((DataType) -> Void)?
    
        // We can set the delegate using any implementer of the protocol
        // But it has to be called `controller.setDelegate(self)`.
        public func setDelegate<Delegate: GenericTableControllerDelegate>(_ d: Delegate?)
            where Delegate.DataType == DataType {
                if let d = d {
                    didSelect = { [weak d, weak self] in
                        if let self = self { d?.controller(controller: self, didSelect: $0) }
                    }
                } else {
                    didSelect = nil
                }
        }
    
        var data = [DataType]()
    
        // and here, just call our internal method
        open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let item = data[indexPath.row]
            didSelect?(item)
        }
    }
    

    This is a useful technique to understand, but I probably wouldn't use it in most cases. There's definitely a headache as you add more methods, if those methods reference DataType. You require a lot of boilerplate. Note that there's a bit of messiness due to passing self to the delegate method. That's something delegate methods need, but closures do not (you can always capture the controller in the closure if the closure needs it).

    As you explore this kind of reusable code, I encourage you to think more about encapsulating strategies rather than about objects and delegate protocols. An example of encapsulating a strategy would be to have a SelectionHandler type that you hand to the controller:

    struct SelectionHandler<Element> {
        let didSelect: (Element) -> Void
    }
    

    With that, you can build simple strategies like "print it out:"

    extension SelectionHandler {
        static func printSelection() -> SelectionHandler {
            return SelectionHandler { print($0) }
        }
    }
    

    Or more interestingly, update a label:

    static func update(label: UILabel) -> SelectionHandler {
        return SelectionHandler { [weak label] in label?.text = "\($0)" }
    }
    

    So then you get code like:

    controller.selectionHandler = .update(label: self.nameLabel)
    

    Or, even more interestingly, you can build higher-order types:

    static func combine(_ handlers: [SelectionHandler]) -> SelectionHandler {
        return SelectionHandler {
            for handler in handlers {
                handler.didSelect($0)
            }
        }
    }
    
    static func trace(_ handler: SelectionHandler) -> SelectionHandler {
        return .combine([.printSelection(), handler])
    }
    
    controller.selectionHandler = .trace(.update(label: self.nameLabel))
    

    This approach composes much more powerfully than delegation, and starts to unlock the real advantages of Swift.