beeware / rubicon-objc

A bridge interface between Python and Objective-C.
https://rubicon-objc.readthedocs.io
Other
268 stars 52 forks source link

Instance properties defined by protocol on superclass are not autodiscovered #96

Open freakboy3742 opened 6 years ago

freakboy3742 commented 6 years ago

On iOS, UITextField defines a keyboardType property. However, this property doesn't appear to get picked up by Rubicon.

Other properties (like borderStyle) work fine.

If you manually invoke send_message(obj, 'setKeyboardType:', ...), it works, too.

So - there's evidently something about the property discovery process.

A little digging revealed that the keyboardType() and setKeyboardType() accessor and mutator aren't found by cache_property_methods(). This may be due to the fact that they're defined on the UITextInputTraits protocol, rather than on a direct superclass of UITextField.

dgelessus commented 6 years ago

Hm, it seems like calls to keyboardType are handled dynamically. The keyboardType property does exist on UITextField, but there is no keyboardType method. Instead, UITextField overrides forwardingTargetForSelector:. When called with SEL("keyboardType") as an argument, it returns a UITextInputTraits object, which does have a statically provided keyboardType method. (The UITextInputTraits here is a class, apparently undocumented, and not the protocol of the same name.)

When calling a method with Rubicon using Python method call syntax, the method is looked up on the class (in the attribute access) and then called later (when the ObjCBoundMethod object is called). This doesn't work for dynamic methods, since they don't exist on the class. On the other hand, send_message uses objc_msgSend and friends to send a method call to the object directly, which triggers the whole dynamic method call process.

Here are some results of playing around with this in Pythonista (iPhone SE, iOS 10.3.3). I used this library that I wrote a while ago since it has better reflection/introspection support than Rubicon does at the moment. (Eventually I want to port that over to Rubicon, or implement something similar, when I have the time...)

>>> import objc
>>> objc.classes.UITextField
Out[0] = objc.api.Class('UITextField')
>>> objc.classes.UITextField.instance_properties_declared["keyboardType"]
Out[2] = <objc.api.Property 'keyboardType', attributes b'Tq,D,N'>
>>> objc.classes.UITextField.instance_methods_declared["keyboardType"]
Traceback (most recent call last): <snip>
builtins.KeyError: objc.api.Selector('keyboardType')
>>> objc.classes.UITextField.instance_methods["keyboardType"]
Traceback (most recent call last): <snip>
builtins.KeyError: 'keyboardType'
>>> tf = objc.classes.UITextField.new()
>>> tf.methods["keyboardType"]
Traceback (most recent call last): <snip>
builtins.KeyError: 'keyboardType'
>>> tf.keyboardType()
Traceback (most recent call last): <snip>
builtins.AttributeError: keyboardType
>>> tf.msg_send("keyboardType")
Traceback (most recent call last): <snip>
builtins.ValueError: No method found for selector b'keyboardType', cannot infer restype and argtypes
>>> tf.msg_send("keyboardType", restype="long long", argtypes=[])
Out[5] = 0
>>> tf.methods["keyboardType"]
Traceback (most recent call last): <snip>
builtins.KeyError: 'keyboardType'
>>> tf.forwardingTargetForSelector_(objc.Selector("keyboardType"))
Out[498] = <objc.api.ID wrapping UITextInputTraits at <cdata 'id_s *' 0x1741d1df0>: <UITextInputTraits : 0x1741d1df0>
public
   autocapitalization:                  2
   autocorrection:                      0
   spellchecking:                       0
   keyboard type:                       0
   kb appearance:                       0
   <snip>
>
>>> _.keyboardType()
Out[499] = 0