如何让实例或 return 类型的泛型符合具有关联值的协议

How can have an instance or a return type of a generic type conforming to a protocol with associated value

所以我不知道标题是否正确解决了我的问题,但事情是这样的:

我有一个枚举,用于识别几种类型的 table 视图单元格。每种类型的细胞都有自己的 class。我已经使其中的每一个都符合协议但具有关联的类型。我现在正在尝试创建一种方法来拥有那些 classes 的实例并任意使用它们的协议方法和属性。这是一个例子:

enum CellType {
    case a, b, c
   
    func getCellClass() -> ProtocolWithAssociatedType.Type { //Error here
        switch self {
        case .a: return ClassA.self
        case .b: return ClassB.self
        case .c: return ClassC.self
    }
}

此枚举在该行引发了 Protocol 'CreateOrderTableViewCellProtocol' can only be used as a generic constraint because it has Self or associated type requirements 错误。

所以这是我的确切协议,除了名称:

protocol ProtocolWithAssociatedType: UITableViewCell {
    associatedtype Delegate
    var delegate: Delegate? {get set}
    func setupCell()
}

所有classes ClassA, ClassB, ClassC 都符合这个。他们都有自己的委托协议,他们使用 typealias 例如:

protocol ClassADelegate: class {
...
}

class ClassA: UITableViewCell, ProtocolWithAssociatedType {
    typealias Delegate = ClassADelegate
    weak var delegate: ClassADelegate?
    func setupCell() {}

    ...
}

extension ViewController: ClassADelegate {
...
}

所有这些都是为了精简 tableView(...cellForItemAt:...) 和其他类似的方法,因为这个项目中有很多单元格 classes 并且它超出了可读性这一点,因此很难在这个特定的视图控制器上进行任何开发。 我有一个 UITableView 的扩展,用于为那些可重用的单元格创建可重用的单元格,它的可重用 ID 与它的 class' 名称相同,如下所示:

func dequeueReusableCell<C>(_ cellType: C.Type, for indexPath: IndexPath) -> C where C: UITableViewCell {
    let identifier = String(describing: cellType)
        
    guard let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? C else {
        fatalError("Failed to find cell with identifier \"\(identifier)\" of type \"\(C.self)\"")
    }
        return cell
}

所以我愿意像下面这样使用它:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cellType = CellDataArray[indexPath.row].cellType
    let cellClass = cellType.getCellClass()

    let cell = tableView.dequeueReusableCell(cellClass, for: indexPath) \ Here I have to have the cell conform to the protocol somehow

    cell.delegate = self \So here is the associated type which doesn't need to be read but is need to be written only
    cell.setupCell() \Obviously there are going to be some data passed to the cell instance right here
    return cell
}

我已经阅读了如此多的问题和答案,并且我正在尝试其中的每一个来完成这项工作,但我一直无法做到。我试图避免在视图控制器中使用大量功能,并尽可能使事情可修改。所有这些单元格本身就像小视图控制器一样,它们都必须与视图控制器通信。单元格也不是静态的,就像在不同的场合,我必须展示不同的单元格。现在,即使添加一个简单的单元格也是一项艰巨的工作,更不用说创造一个全新的场合了。但是通过这种方法,我正在尝试使事情...模块化。

那么,有什么方法可以做到这一点,而不会时不时地出现运行时崩溃,也不会产生黑洞并导致宇宙终结?

编辑:我已经尝试过通用类型的方式但无法这样做。由于我希望 func getCellClass() 工作的方式,不可能让编译器知道该通用类型是什么。喜欢以下内容:

func getCellClass<C>() -> C.Type where C: ProtocolWithAssociatedValue {
...
}

所以即使我强制将 return 值转换为 C.Type 然后我在调用它时遇到问题只是因为 C 是未知的。

编辑

我已从协议中删除关联值并执行了以下操作:

protocol ProtocolWithAssociatedType: UITableViewCell {
    var _delegate: AnyObject? {get set}
    func setupCell()
}
protocol ClassADelegate: class {
...
}

class ClassA: UITableViewCell, ProtocolWithAssociatedType {
    weak var _delegate: AnyObject? { didSet { delegate = _delegate as? ClassADelegate } }
    private weak var delegate: ClassADelegate?
    func setupCell() {}

    ...
}

extension ViewController: ClassADelegate {
...
}

通过这种方式,协议可用作该枚举函数的 return 类型,并且我可以强制将可重用单元转换为它,因为协议本身符合 UITableViewCell。我构建没有任何问题,但还不能测试(测试服务器在工作时间以外关闭)。当我测试它并且如果它运行没有任何问题时,我将 post 它作为解决方案。

您似乎在尝试将多态行为(即单元格类型数组)与静态行为(具有关联类型的协议)混合,这在当前 Swift 设计中或多或少是不可能的。

具有关联类型的协议是在编译时确定的,而您的 tableView(_:cellForRowAt:) 方法中的代码是在运行时执行的,这使得它与具有关联类型的协议有些不兼容。

let cell = tableView.dequeueReusableCell(cellClass, for: indexPath)

在上面的代码中,您需要能够告诉编译器您希望单元格使用哪种类型,以便注入委托,但这是一个 compile-time 功能,您在运行时需要它,因此不兼容。

恐怕您将不得不为此坚持运行时错误 propagation/handling。除非您愿意更改 delegate 以匹配整组可能的委托,并且在这种情况下不再需要关联类型,这会启用运行时功能。

protocol MyCell: UITableViewCell {
    var delegate: CellDelegate1 & CellDelegate2 ... & CellDelegateN { get set }

    func setupCell(SomeCommonProtocolThatGetsCastedAtRuntime)
}

解决方案

所以经过几天的工作 trial-error 我终于想出了一个解决方案。感谢@Cristik,我有了洞察力并改变了我的方法。

首先我改变了处理代表的方式。每个单元都有不同的代表。现在我只有一个,所有这些细胞都使用。

所以这样我就可以让我的 属性 像下面这样:

protocol CellProtocol: UITableViewCell {
    var delegate: CommonCellDelegate?
    func setupCell(...)
}

对于单元将使用的视图控制器的所有属性和一些常用方法,我已经声明了如下委托协议:

protocol CommonCellDelegate: class {
    var viewControllerProperty1: SomeClass1 { get set }
    var viewControllerProperty2: SomeClass2 { get set }
    ...
    var viewControllerPropertyN: SomeClassN { get set }

    func someCommonFunction1(...) -> ...
    func someCommonFunction2(...) -> ...
    ...
    func someCommonFunctionN(...) -> ...
}

因为我从一开始就打算保持视图控制器的美观和干净,所以我将单元格单独需要的那些不常见的方法放在这些单元格的 class 声明下面作为委托的扩展 属性:

class SomeCellClass: CellProtocol {
    var delegate: CommonCellDelegate?
    func setupCell(...) {
    ...
    }

    ...
}
extension CommonCellDelegate {
    func someMethodForTheCellAbove1(...) -> ... {
    ...
    }
    func someMethodForTheCellAbove2(...) -> ... {
    ...
    }
    .
    .
    .
}

由于我不再需要枚举方法来 return 不同的 classes,我将其稍微更改为 return 每个单元格类型的重用 ID。所以不需要 return class 类型:

enum CellType {
    case cellA, cellB, cellC, ...
   
    func getCellClass() -> String { //Error here
        switch self {
        case .cellA: return String(describing: ClassA.self)
        case .cellB: return String(describing: ClassB.self)
        case .cellC: return String(describing: ClassC.self)
        ...
        // Note: We use the the name of the cell classes as their reuse id's for simplicity.
    }
}

最后,我能够非常轻松地在 tableView(_:cellForRowAt:) 方法中创建这些单元格,而不会在 space-time:

的结构中创建裂口
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cellType = CellDataArray[indexPath.row].cellType
    let cellReuseId = String(describing: cellType.getCellId())

    guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) as? CellProtocol else { fatalError("some fatal error") }

    cell.delegate = self
    cell.setupCell(...)
    return cell
}

通过这种方式,创建新的实现非常非常容易,因为所有需要做的就是创建 class,使 class 符合 CellProtocol,创建一个在枚举中为它添加大小写,随时随地将大小写添加到数组中,注册单元格 ID 和 voilà! 它就在那里!由于委托,单元现在知道所有需要的属性,所以他们可以像使用自己的属性一样轻松地使用这些属性,唯一的区别是它不是 self.something 而是 delegate?.something.

希望我的努力对其他人有所帮助。干杯!