beeware / beeware/rubicon-objc
Can't invoke Protocol methods that collide with object properties
- Dominant language
- Python
- Stars
- 301
- Forks
- 70
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 18
Description
### Describe the bug
If an Obj C object has a property, and implements a Protocol with selectors whose first argument matches that property, it is not possible to invoke the selectors.
### Steps to reproduce
This was discovered when developing beeware/toga#2025; the [iOS DetailedList code in v0.3.1](https://github.com/beeware/toga/blob/v0.3.1/iOS/src/toga_iOS/widgets/detailedlist.py) contains a setup that demonstrates the problem; the [corresponding code in #2025 (to be included in v0.3.2)](https://github.com/beeware/toga/blob/6ec8eb8bb87fb017cae39234906f046303d7197f/iOS/src/toga_iOS/widgets/detailedlist.py) contains a workaround.
To demonstrate the problem: on an instance of DetailedList, use Python code to programmatically select a row on the DetailedList. This would be `[controller.tableView.delegate tableView:controller.tableView didSelectRowAtIndexPath:...]` in ObjC, where `controller` is the instance of TogaTableViewController, which maps to `controller.tableView.delegate(controller.tableView, didSelectRowAtIndexPath:...)` in Rubicon.
This will fail on v0.3.1, but pass on the beeware/toga#2025 branch, because of the way the widget is constructed.
The iOS backend uses a UITableViewController with a default UITableView to represent a DetailedList; the UITableViewController has a `tableView` property describing the view that it is controlling.
A UITableView can specify a UITableViewDelegate; the delegate responds to a number of selectors, including `tableView:didSelectRowAtIndexPath:`.
In v0.3.1, a subclass of UITableViewController was used (`TogaTableViewController`) that also acted as the delegate. When an attempt is made to invoke `tableView:didSelectRowAtIndexPath:`, you get the error:
```
Traceback (most recent call last):
...
self.native.delegate.tableView(self.native, didSelectRowAtIndexPath=path)
TypeError: 'ObjCInstance' object is not callable
```
`self.native.delegate` returns the delegate, which is a `TogaTableViewController` instance; since this is a subclass of UITableViewController, it has a `tableView` property, so the Python `tableView` attribute is an ObjCInstance, not an ObjCMethod. It doesn't matter if you also explicitly declare the delegate as implementing the `UITableViewDelegate` protocol. You also can't pass the `tableView:didSelectRowAtIndexPath:` message directly.
The beeware/toga#2025 branch fixes this by using a raw UITableViewController, and making the delegate a subclass of NSObject. This removes the ambiguity between `[UITableViewController tableView]` and `tableView:didSelectRowAtIndexPath:`
This limitation doesn't exist on ObjC code - other bugs notwithstanding, the v0.3.1 branch *works*, and the UITableView is able to invoke the delegate methods defined in Python, no matter how it's defined. It only emerged in testing when we were programmatically invoking these methods *from Python*.
This also doesn't affect methods like `tableView:cellForRowAtIndexPath:` which are defined as part of the UITableViewDataSource protocol - or, at least, there's a different workaround. If you explicitly invoke the method on the `controller.dataSource`, rather than on the `controller` object directly, the method resolves. This appears to be because the data source is *implicitly* set by constructing the UITableViewController; if you *explicitly* set the `dataSource` property of the controller, the same problem manifests.
### Expected behavior
It should be possible to retrieve the `tableView` property *and* invoke the `tableView:didSelectRowAtIndexPath:` selector on a delegate object.
### Screenshots
_No response_
### Environment
- Operating System: iOS 16.4
- Python version: 3.10
- Software versions:
- Rubicon: 0.4.6
- Toga: 0.3.1
### Logs
```
```
### Additional context
There's probably an argument to be made that this is a documentation issue. Mapping from ObjC to Python is always going to be leaky, and there's workarounds available.
Another approach would be to require explicit casting to the Protocol in this case.
Contributor guide
Research direction
Start with the reproduction in iOS/src/toga_iOS/widgets/detailedlist.py, comparing the v0.3.1 setup with the workaround in the beeware/toga#2025 branch. Reproduce the collision between the tableView property and tableView:didSelectRowAtIndexPath: through Rubicon, then inspect selector and attribute resolution. Done means both the property remains retrievable and the protocol selector can be invoked from Python.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- objective-c, python
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100