My application has a common base class for all table controllers, and I'm experiencing a strange bug when I define a generic subclass of that table controller base class. The method numberOfSections(in:)
never gets called if and only if my subclass is generic.
Below is the smallest reproduction I could make:
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 {
}
Here's the very simple generic subclass:
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)
}
}
If I create a simple app that does nothing but display 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
}
}
The table draws correctly but numberOfSections(in:)
is never called! As a result, the table only shows one section (presumably because, according to the docs, UITableView
uses 1 for this value if the method isn't implemented).
However, if I remove the generic declaration from the class:
class ViewController: CustomTableViewController {
let data: Int
init(data: Int) {
....
}
// ...
}
then numberOfSections
DOES get called!
This behavior doesn't make any sense to me. I can work around it by defining numberOfSections
in CustomTableViewController
and then having ViewController
explicitly override that function, but that doesn't seem like the correct solution: I would have to do it for any method in UITableViewDataSource
that has this problem.
If you provide default implementations of the delegate methods (numberOfSections(in:)
, etc.) in your base class and override them in your subclasses where appropriate, they will be called:
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
}
...
An alternative approach would be to develop your base class based on UITableViewController
which already brings most of the things you need (a table view, delegate conformance, and default implementations of the delegate methods).
EDIT
As pointed out in the comments, the main point of my solution is of course what the OP explicitly didn't want to do, sorry for that... in my defense, it was a lengthy post ;) Still, until someone with a deeper understanding of Swift's type system comes around and sheds some light on the issue, I'm afraid that it still is the best thing you can do if you don't wand to fall back to UITableViewController
.
This is a bug / shortcoming within the generic subsystem of swift, in conjunction with optional (and therefore: @objc
) protocol functions.
Solution first
You'll have to specify @objc
for all the optional protocol implementations in your subclass. If there is a naming difference between the Objective C selector and the swift function name, you'll also have to specify the Objective C selector name in parantheses like @objc (numberOfSectionsInTableView:)
@objc (numberOfSectionsInTableView:)
func numberOfSections(in tableView: UITableView) -> Int {
// this is now called!
print("called numberOfSections")
return 1
}
For non-generic subclasses, this already has been fixed in Swift 4, but obviously not for generic subclasses.
Reproduce
You can reproduce it quite easy in a 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)")
}
}
Now when we use it without generics, everything works find:
let normal = NormalSubclass(val:42)
normal.doIt() // Doing 42
normal.baseMethod() // Doing 42
When using a generic subclass, the baseMethod
call crashes:
let generic = GenericSubclass(val:5)
generic.doIt() // Doing 5
generic.baseMethod() // error: Execution was interrupted, reason: signal SIGABRT.
Interestingly, the doIt
selector could not be found in the GenericSubclass
, although we just called it before:
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'
(error message taken from a "real" project)
So somehow the selector (e.g. Objective C method name) cannot be found.
Workaround: Add @objc
to the subclass, as before. In this case we don't even need to specify a distinct method name, since the swift func name equals the Objective C selector name:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With