Generic view controller won't work with delega

2019-09-08 15:34发布

问题:

I already posted a question but it was not clear about what I want. As @AlainT suggested, I filed a new one.

I have a typealias tuple

public typealias MyTuple<T> = (key: T, value: String)

A protocol:

public protocol VCADelegate: class {
    associatedtype T
    func didSelectData(_ selectedData: MyTuple<T>)
}

A view controller (VCA) with a table view

class VCA<T>: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var dataList = [MyTuple<T>]()
    weak var delegate: VCADelegate? // Error: can only be used as a generic constraint    

    // ...

    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        delegate?.didSelectData(dataList[indexPath.row])
    }
}

In another view controller (VCB), I create a VCA and pass through a dataList

func callVCA() {
    let vcA = VCA<String>()
    vcA.dataList = [(key: "1", value:"Value 1"),
                    (key: "2", value:"Value 2")]
} 

What I want to do is to have a dataList without knowing key's data type in VCA. Only when VCB calls VCA then I know the data type of the key. Creating a generic view controller will cause an issue with delegate. Any way to solve this problem without having to change to closure completion?

The other issue of using a generic view controller is I can't extend it. Any idea?

回答1:

This is a standard type-erasure situation, though in this particular case I'd just pass a closure (since there's only one method).

Create a type eraser instead of a protocol:

public struct AnyVCADelegate<T> {
    let _didSelectData: (MyTuple<T>) -> Void
    func didSelectData(_ selectedData: MyTuple<T>) { _didSelectData(selectedData)}
    init<Delegate: VCADelegate>(delegate: Delegate) where Delegate.T == T {
        _didSelectData = delegate.didSelectData
    }
}

Use that instead of a delegate:

class VCA<T>: UIViewController, UITableViewDataSource UITableViewDelegate {

    var dataList = [MyTuple<T>]()
    var delegate: AnyVCADelegate<T>?

    // ...

    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        delegate?.didSelectData(dataList[indexPath.row])
    }
}

Your underlying problem is that protocols with associated types are not proper types themselves. They're only type constraints. If you want to keep it a PAT, that's fine, but then you have to make VCA generic over the Delegate:

class VCA<Delegate: VCADelegate>: UIViewController, UITableViewDelegate {

    var dataList = [MyTuple<Delegate.T>]()
    weak var delegate: Delegate?

    init(delegate: Delegate?) {
        self.delegate = delegate
        super.init(nibName: nil, bundle: nil)
    }

    required init(coder: NSCoder) { super.init(nibName: nil, bundle: nil) }

    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        delegate?.didSelectData(dataList[indexPath.row])
    }
}

class VCB: UIViewController, VCADelegate {

    func didSelectData(_ selectedData: MyTuple<String>) {}

    func callVCA() {
        let vcA = VCA(delegate: self)
        vcA.dataList = [(key: "1", value:"Cinnamon"),
                        (key: "2", value:"Cloves")]
    }
}

As a rule, protocols with associated types (PATs) are a very powerful, but special-purpose tool. They aren't a replacement for generics (which are a general purpose tool).

For this particular problem, though, I'd probably just pass a closure. All a type eraser is (usually) is a struct filled with closures. (Some day the compiler will probably just write them for us, and much of this issue will go away and PATs will be useful in day-to-day code, but for now it doesn't.)