调度问题与自定义表视图控制器的通用子类

时间:2018-01-11 20:58:53

标签: ios swift uitableview generics

我的应用程序有一个适用于所有表控制器的公共基类,当我定义该表控制器基类的通用子类时,我遇到了一个奇怪的错误。当且仅当我的子类是通用的时,才会调用方法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被召唤!

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

2 个答案:

答案 0 :(得分:10)

这是swift通用子系统中的一个错误/缺点,与可选(因此:@objc)协议功能相结合。

首先解决方案

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

之类的parantheses中指定Objective C选择器名称
@objc (numberOfSectionsInTableView:)
func numberOfSections(in tableView: UITableView) -> Int {
    // this is now called!
    print("called numberOfSections")
    return 1
}

对于非泛型子类,这已在Swift 4中修复,但显然不适用于泛型子类。

<强>重现

您可以在操场上轻松复制:

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]:无法识别的选择器发送到实例0x60800001a8d0   2018-01-14 22:23:16.243702 + 0100 GenericTableViewControllerSubclass [13234:3471799] ***由于未捕获的异常'NSInvalidArgumentException'终止应用程序,原因:' - [ TtGC34GenericTableViewControllerSubclass15GenericSubclassSi doIt]:发送无法识别的选择器例如0x60800001a8d0'

(从“真实”项目中获取的错误消息)

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

答案 1 :(得分:2)

如果在基类中提供委托方法的默认实现(numberOfSections(in:)等)并在适当的子类中覆盖它们,则会调用它们:

extension BaseTableViewController: UITableViewDataSource {
  func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }
  ...

class ViewController<X>: BaseTableViewController {
  ...
  override func numberOfSections(in tableView: UITableView) -> Int {
    // now this method gets called :)
    print("called numberOfSections")
    return 1
  }
  ...

另一种方法是基于UITableViewController开发基类,它已经带来了您需要的大部分内容(表视图,委托一致性和委托方法的默认实现)。

修改

正如评论中所指出的,我的解决方案的主要内容当然是OP明确没有想要做什么,对不起...在我的辩护中,它是一个冗长的帖子;)尽管如此,直到有人对Swift的类型系统有了更深入的了解并且对这个问题有所了解,我担心如果你仍然是你能做的最好的事情。不要畏缩回到UITableViewController