Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Help calling "addTarget:action:forControlEvents:" from objc_util
-
So I see part of the problem: I was defining the target function "controlTouchEvent" inside a class definition, which unsurprisingly messed up the selector name...if I define the function globally, it works.
Therefore I have two questions:
- What do I need to provide as the selector name for a function defined within a class (not a python instance method, just a function defined within a class definition) as in:
class Thing(object): def __init__(self, button, ...): def targetFunction(_self, _cmd): print "target function" TargetClass = objc_util.create_objc_class("TargetClass", methods=[targetFunction]) self.target = TargetClass.new().autorelease() objcbutton = objc_util.ObjCInstance(button) objcbutton = objcbutton.subviews()[0] objcbutton.addTarget_action_forControlEvents_(self.target, "targetFunction", 0x00000FFF)
The above is what I was doing, but it was giving me the "No method found for selector" error every time.
Doing it this way works:def targetFunction(_self, _cmd): print "target function" TargetClass = objc_util.create_objc_class("TargetClass", methods=[targetFunction]) target = TargetClass.new().autorelease() class Thing(object): def __init__(self, button, ...): objcbutton = objc_util.ObjCInstance(button) objcbutton = objcbutton.subviews()[0] objcbutton.addTarget_action_forControlEvents_(target, "targetFunction", 0x00000FFF)
So how do you reference a selector defined within a class? Or is that just not possible?
- There appear to be three overloaded versions of the action method which can be provided to "addTarget_action_forControlEvents":
https://developer.apple.com/documentation/uikit/uicontrol?language=objc#1943645
- (IBAction)doSomething; - (IBAction)doSomething:(id)sender; - (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event;
But it seems like I can only provide the first definition, the one which takes no arguments. If I try to supply a function which takes a "sender" argument or both "sender" and "event", I get a traceback saying that the function only takes 2 arguments. Does the selector name change when it takes more arguments than the first two, i.e. doSomething_sender or doSomething_sender_event?
-
Well...I think I was bitten by the same "changes in objective-c remain across script runs" issue that I've gotten bitten by before. If I force quit and restart Pythonista, the function-defined-within-class works as expected. And for the second issue: the selector must be named to match the parameters...so for the third one, with both sender and event:
Name the target function:
def controlTouchEvent_sender_event(_self, _cmd, sender, event):
and then in the actual call to establish the target selector:
objcinstance.addTarget_action_forControlEvents_(self._target, "controlTouchEvent:sender:event", 0x00000FFF)
So it seems I muddled through it after all.
-
@shinyformica, thank you for a very interesting and educational series of posts – they read a bit like a detective novel.
Being an objc muddler myself, I am wondering why your method and selector do not need to have the trailing underscore/colon.
-
@mikael ...I...don't know. It seems possible the objc_util module is forgiving about that kind of thing when providing a selector name string. I didn't even fully grasp how the parameters needed to be defined or named in my function...the objective-c definition looks like:
- (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event;
so I just assumed it would need to have a slot for a "sender" and "event" parameter, along with the default "_self" and "_cmd" parameters, and voila.
Some of this still feels a bit like magic and rainbows to me...
-
@mikael said:
Being an objc muddler myself, I am wondering why your method and selector do not need to have the trailing underscore/colon.
Objective-C's requirements about where to put colons are only enforced at compile time (they are a part of the language syntax). At runtime it doesn't matter if the colons match up with the method arguments (at that point everything has been translated to regular C function pointer calls), so the runtime never checks the selector names in any way. It's similar to how Python lets you say
setattr(obj, "funny attribute name!", 42)
, even though you can't usefunny attribute name!
as an attribute in your source code.The underscores in the function name do matter to
create_objc_class
, it counts the number of underscores in the name to figure out how many parameters the method has. So even thoughcontrolTouchEvent_sender_event
is not really a correct Objective-C method name, it has two underscores, which matches the number of arguments that the Python function takes (in addition to_self
and_cmd
), so the call works in the end.@shinyformica said:
I didn't even fully grasp how the parameters needed to be defined or named in my function...
The example method (
(IBAction)doSomething:(id)sender forEvent:(UIEvent *)event
) would translate todoSomething:forEvent:
as a selector, ordoSomething_event_
as a Python function name. Only the word to the left of each colon is part of the selector. The word to the right of each parameter type is the internal parameter name - this only matters if you're implementing the method in Objective-C source code, you can ignore it when translating the method name toobjc_util
.In this case it doesn't matter though what exactly the method name is. You only need to make sure that the underscore/colon count in your function name matches how many arguments the function takes, otherwise
objc_util
won't call your function with the correct number of arguments. -
@dgelessus thanks for the very informative response!
-
out of curiosity, what is the use case here? Another option might be to have a transparent overlay, which implements touch_moved, etc, then uses the objc hitTest:withEvent: on the view underneath, which then returns the objc view that wants to handle the touch. Then you simply forward the event onto it, doing whatever other logging or hijacking you wanted.
-
@JonB yeah, I thought of doing something of that nature...but that would require some other complicated handling that doing things this way removes. I'm not really hijacking the touch events, but I am keeping track of what control is currently being interacted with for other reasons. If there is a more general, global, robust way of determining the currently active control, meaning the control or controls which the user is currently manipulating, then I definitely would want to do that instead of this.
I also needed to make this more-or-less transparent to whether the control is a custom ui.View or a standard wrapped UIKit control, from an outside API perspective.
This way does work, but I'm always completely open to better ideas.
-
I guess I'm trying to understand the end goal. This is for some sort of debugging or logging purpose? A prank keylogger app?
Anything that has an python action or delegate includes a sender argument that gives you access to the item being touched, so you could use a decorator on those methods which handles the logging...
One approach would be to add a gesture recognizer on the application keyWindow, that does not cancel touch events.
https://stackoverflow.com/questions/8444184/listen-to-all-touch-events-in-an-ios-app
-
The use case is a little complicated, and very task-specific:
- app is constantly listening for incoming messages
- manipulating controls in the view sends outgoing messages
- some incoming messages will try to set the value of a control being manipulated
- this leads to "fighting" between the user and the system
- knowing which control or controls are being interacted with, we can filter the incoming messages so they don't attempt to set controls actively being manipulated, effectively "debouncing" the signal
I like the idea of a transparent layer...but I wanted to be more specific about what I considered "manipulating" the control...with this, I can say exactly what I consider to be user interaction for these purposes.
Anyway, I am using the sender argument coming from the addTarget_action_forControlEvents() call, which is how I'm keying which controls are marked active. For my purposes it is working.Unfortunately, it seems like the various Pythonista-wrapped UIControls have different ways, or sometimes no way, of getting access to the actual UIControl instance they wrap, which makes it a little complicated to call the addTarget_ objectiveC method to install the monitor.