Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Class does not forward delegate calls to concrete subclass

Given the following.

protocol EntityType {
    var displayString: String { get }
}

extension String: EntityType {
    var displayString: String { return self }
}

class GenericListViewController<Entity>: UIViewController, UITableViewDataSource, UITableViewDelegate where Entity: EntityType {
    let items: [Entity]

    let tableView: UITableView

    init(items: [Entity]) {
        self.items = items
        self.tableView = UITableView()

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

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

    override func loadView() {
        super.loadView()

        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        ])

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        tableView.dataSource = self
        tableView.delegate = self

    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row].displayString

        return cell
    }

//    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//        
//    }
}

class StringListViewController: GenericListViewController<String> {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("selected: \(items[indexPath.row])")
    }
}

Why isn't tableView:didSelectRowAt: being called in the concrete subclass? This works fine for non-generic types or when the parent class has an implementation and the sub-class overrides it. Is this expected behavior, Swift bug or am I missing something?

like image 961
Chris Wagner Avatar asked Feb 06 '17 19:02

Chris Wagner


1 Answers

It does seem like a possible bug. My guess it that it might have something to do with the fact that Swift generics are not visible to the Objective-C runtime, so even though your methods implemented directly in GenericListViewController<Entity> are being called, there may be some buggy behavior between the Swift and Obj-C runtimes trying to figure out overrides. Definitely worth a bug report.

I will note though, that in strict OOP abstract superclasses generally do not conform to protocols themselves, they simply provide default implementations. It's still up to the concrete subclasses to declare protocol conformance and fill in any missing implementations.

In the case of your code above, your GenericListViewController<Entity> class should not conform to UITableViewDataSource or UITableViewDelegate. It simply provides the default method implementations that would allow a concrete subclass to conform without having to rewrite those method implementations (unless override is desired). Your StringListViewController should be the one declaring conformance to the two protocols. If you modify your code to do that, it will actually work as expected.

This doesn't change the fact that you have probably discovered a bug in Swift / Obj-C interop, though. I believe that what you have currently should work, although it's not the strict OOP way of handling protocol conformance.

like image 60
Daniel Hall Avatar answered Oct 25 '22 18:10

Daniel Hall