Проблема отправки с общим подклассом настраиваемого контроллера табличного представления

Мое приложение имеет общий базовый класс для всех контроллеров таблиц, и я испытываю странную ошибку, когда я определяю общий подкласс этого базового класса контроллера. Метод 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, который имеет эту проблему.

Ответ 1

Это ошибка/недостаток в общей подсистеме swift в сочетании с необязательными (и поэтому: @objc) функциями протокола.

Решение сначала

Вам нужно указать @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, но, очевидно, не для общих подклассов.

Воспроизведите

Вы можете легко воспроизвести его на игровой площадке:

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 в подкласс, как и раньше. В этом случае нам даже не нужно указывать отдельное имя метода, так как имя быстрой функции равно имени селектора 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 

Ответ 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 явно не хотел делать, извините за это... в моей защите это была длинная запись;) Тем не менее, до тех пор, пока кто-то с более глубоким пониманием системы типа Свифт приходит и проливает некоторый свет на эту проблему, я боюсь, что это все-таки самое лучшее, что вы можете сделать, если не сделаете палочку, чтобы вернуться к UITableViewController.

Ответ 3

Замените init на метод coder:

required init?(coder aDecoder: NSCoder) {
   super.init(coder: aDecoder)
}

Собственно, если у вас есть ваша ячейка, созданная в Storyboard - я считаю, что она должна быть привязана к tableView, на которой вы пытаетесь ее создать. И вы можете удалить оба метода init, если вы не выполняете там никакой логики.

Приветствия из Германии