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