ios - 自定义 TableView Controller 的通用子类的调度问题

标签 ios swift uitableview generics

我的应用程序有一个用于所有表 Controller 的公共(public)基类,当我定义该表 Controller 基类的通用子类时,我遇到了一个奇怪的错误。当且仅当我的子类是通用的时,方法 numberOfSections(in:) 永远不会被调用。

下面是我能做出的最小复制品:

class BaseTableViewController: UIViewController {
  let tableView: UITableView

  init(style: UITableViewStyle) {
    self.tableView = UITableView(frame: .zero, style: style)

    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: - Overridden methods

  override func viewDidLoad() {
    super. viewDidLoad()

    self.tableView.frame = self.view.bounds
    self.tableView.delegate = self
    self.tableView.dataSource = self

    self.view.addSubview(self.tableView)
  }
}

extension BaseTableViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    return UITableViewCell(style: .default, reuseIdentifier: nil)
  }
}

extension BaseTableViewController: UITableViewDelegate {
}

这是非常简单的通用子类:

class ViewController<X>: BaseTableViewController {
  let data: X

  init(data: X) {
    self.data = data
    super.init(style: .grouped)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  func numberOfSections(in tableView: UITableView) -> Int {
    // THIS IS NEVER CALLED!
    print("called numberOfSections")
    return 1
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    print("called numberOfRows for section \(section)")
    return 2
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    print("cellFor: (\(indexPath.section), \(indexPath.row))")
    let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
    cell.textLabel!.text = "foo \(indexPath.row) \(String(describing: self.data))"

    return cell
  }

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print("didSelect: (\(indexPath.section), \(indexPath.row))")
    self.tableView.deselectRow(at: indexPath, animated: true)
  }
}

如果我创建一个简单的应用程序,它只显示 ViewController:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    self.window = UIWindow(frame: UIScreen.main.bounds)

    let nav = UINavigationController(rootViewController: ViewController(data: 3))
    self.window?.rootViewController = nav
    self.window?.makeKeyAndVisible()
    return true
  }
}

表格绘制正确但 numberOfSections(in:) 从未被调用!因此,该表仅显示一个部分(可能是因为根据文档,如果未实现该方法,UITableView 使用 1 作为此值)。

但是,如果我从类中删除通用声明:

class ViewController: CustomTableViewController {
  let data: Int

  init(data: Int) {
  ....
  }

  // ...
}

然后 numberOfSections 确实被调用了!

这种行为对我来说没有任何意义。我可以通过在 CustomTableViewController 中定义 numberOfSections 然后让 ViewController 显式覆盖该函数来解决这个问题,但这似乎不是正确的解决方案: 对于 UITableViewDataSource 中存在此问题的任何方法,我都必须这样做。

最佳答案

这是 swift 的通用子系统中的一个错误/缺点,与可选的(因此:@objc)协议(protocol)函数一起使用。

解决方案第一

您必须为子类中的所有 可选协议(protocol)实现指定@objc。如果 Objective C 选择器和 swift 函数名称之间存在命名差异,您还必须在括号中指定 Objective C 选择器名称,例如 @objc (numberOfSectionsInTableView:)

@objc (numberOfSectionsInTableView:)
func numberOfSections(in tableView: UITableView) -> Int {
    // this is now called!
    print("called numberOfSections")
    return 1
}

对于非泛型子类,这已经在 Swift 4 中得到修复,但显然不是针对泛型子类。

复制

您可以在 playground 中很容易地重现它:

import Foundation

@objc protocol DoItProtocol {
    @objc optional func doIt()
}

class Base : NSObject, DoItProtocol {
    func baseMethod() {
        let theDoer = self as DoItProtocol
        theDoer.doIt!() // Will crash if used by GenericSubclass<X>
    }
}

class NormalSubclass : Base {
    var value:Int

    init(val:Int) {
        self.value = val
    }

    func doIt() {
        print("Doing \(value)")
    }
}

class GenericSubclass<X> : Base {
    var value:X

    init(val:X) {
        self.value = val
    }

    func doIt() {
        print("Doing \(value)")
    }
}

现在当我们在没有泛型的情况下使用它时,一切正常发现:

let normal = NormalSubclass(val:42)
normal.doIt()         // Doing 42
normal.baseMethod()   // Doing 42

使用通用子类时,baseMethod 调用崩溃:

let generic = GenericSubclass(val:5)
generic.doIt()       // Doing 5
generic.baseMethod() // error: Execution was interrupted, reason: signal SIGABRT.

有趣的是,doIt 选择器在GenericSubclass 中找不到,尽管我们之前只是调用了它:

2018-01-14 22:23:16.234745+0100 GenericTableViewControllerSubclass[13234:3471799] -[TtGC34GenericTableViewControllerSubclass15GenericSubclassSi doIt]: unrecognized selector sent to instance 0x60800001a8d0 2018-01-14 22:23:16.243702+0100 GenericTableViewControllerSubclass[13234:3471799] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TtGC34GenericTableViewControllerSubclass15GenericSubclassSi doIt]: unrecognized selector sent to instance 0x60800001a8d0'

(取自“真实”项目的错误信息)

因此无法找到选择器(例如 Objective C 方法名称)。 解决方法:像以前一样,将 @objc 添加到子类中。在这种情况下,我们甚至不需要指定一个不同的方法名称,因为 swift 函数名称等于 Objective C 选择器名称:

class GenericSubclass<X> : Base {
    var value:X

    init(val:X) {
        self.value = val
    }

    @objc
    func doIt() {
        print("Doing \(value)")
    }
}

let generic = GenericSubclass(val:5)
generic.doIt()       // Doing 5
generic.baseMethod() // Doing 5 

关于ios - 自定义 TableView Controller 的通用子类的调度问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48215689/

相关文章:

ios - -[UITableView reloadData] 是异步的还是同步的?

ios - 在 UISearchDisplayController 中检测对 "no displayed search results"的点击

ios - 当文本下降到 Swift 中的键盘级别时,如何在我的 TextView 中实现自动滚动?

ios - 为什么 Xcode 使用 __block 进入未调用的函数断点?

iphone - 一个 Action 或来自多个 Action 的多个方法调用

ios - MKPolyline 未在 map 上绘制。我该如何解决这个问题?

ios - 具有一个 float 标题的 Tableview 多个部分

ios - pickerView 不同的行高

ios - 在 UITableViewController 中设置默认选中的单元格

iphone - 如何从应用程序委托(delegate)、 Storyboard、iOS6 获取我的 View Controller 的实例