Believe it or not, I don't really know what "gist" is...I'll go look it up.
During touch-moved (or handle_pan() when this is being done via a Gestures callback) on one of these custom controls (though they're all slightly different), it basically:
- updates the control's value by doing some simple calculation
- calls a method which causes a that new value to be wrapped up with a message and queued onto the outbound message queue.
And that's really it. It also generally calls set_needs_display() when the value changes, to make sure the UI reflects the interaction and new value. These things don't take much time at all, but I always see the same thing in both the console output and the "listening" client on a different machine which is receiving these messages: very stuttery output during interaction, rarely receiving a message while the control is being interacted with - though if I pause moving without releasing the touch, suddenly a big chunk of queued-up messages arrive.
Anyway, for the moment I've made things ok for my purposes by having the message send be synchronous, so it happens immediately when the control value changes and isn't queued for send. But I still would love to know if this is something I'm misunderstanding about how the iOS UI+Pythonista UI thread+main thread synchronization works.
One other thing: I tried putting time.sleep(0) in various places during touch interaction and message queueing, to see if I can make sure the other threads are given a chance to run while interacting, but it didn't seem to help...I don't really want to be using events or locks to directly control the timing of thread execution.
I actually thought I was doing things in a pretty reasonable way, but unfortunately the result I'm seeing isn't smooth threaded running, and I'm not entirely sure why. The code is too complex to post, but here's a schematic of the structure:
- Has a main view which uses update() and an update_interval = 0.01
- The update() method of that view pulls incoming data off a thread-safe queue and processes it (processing time is very small)
- There are two standard threading.Threads running: one listens forever on a port for incoming messages via a select.select() call, placing them onto the queue the main view's update() method is reading from, and the other consumes items from another queue for sending out over a socket.
I'm glossing over some details about how interaction with widgets causes messages to end up on the queue for sending, and how incoming messages are processed to update the UI, but those things all operate synchronously, and are very fast.
For the most part the system operates as I expect: threads run, incoming messages show up in the UI via update(), tapping controls which send messages are sent by the sending thread, etc. The problem arises when I have a custom control which needs to send messages continuously while being interacted with. So a user holds down a finger and moves it around and it needs to keep sending out messages until the touch is ended. There I see very jittery performance for the sending thread (sometimes no messages sent, sometimes big chunks of queued-up messages, occasionally smaller chunks). I was expecting that the UI interaction would not "interfere" with the other threads, in the sense that unless it was actively calling draw() or one of the touch*() methods or Gestures methods, it would allow the threads to run...but it seems like during a touch interaction on a custom view, other threads aren't really given a chance to run? I'll try to put together a much simpler example to really test this out.
What are the best approaches to having a responsive UI which also allows background threads to run relatively smoothly in Pythonista apps?
More specifically, I'm having trouble in a setup where a user interacts with a custom widget, one where they touch and drag around on it to send messages. I was hoping to have those messages placed into a thread-safe queue and then be processed by a thread running separately from the main/ui threads.
For me, using either the Gestures module, or the touch_began(), touch_moved() and touch_ended() methods of a custom view, the consumer thread which processes the messages very rarely gets a chance to run. Even if I put in time.sleep(0) calls after each time the touch interaction places a message on the queue, trying to force the main thread to release control, I find the background thread seldom gets any time to process...if I pause while interacting, drag around for a moment and then stop, the background thread suddenly gets a chance to process all the queued up messages. But while dragging, almost never.
So, is this just the GIL being it's problematic self? Is there a particular design-pattern which works very well for this scenario, and allows the user to interact relatively smoothly while still letting a background thread perform tasks? Is there some use of in_background(), or the objc_util on_main_thread() which would make this work better?
I'm not married to the threaded system I'm using, if there's a really good asynchronous or semi-asynchronous way of doing things. This is in Python2.7, by the way...so asyncio is not available, and since it's iOS there's no option to use multiprocessing.
Having just gone through the annoyingly difficult process of debugging a crash happening somewhere in my code, but not directly caused by python (not a traceback...something I'm doing in objc or elsewhere), I wonder:
Does pythonista record anything to a log when it crashes running python code? Is there some way to generate or examine such a log?
I swear I read a post on here at one point a while back which mentioned something about crash logs...but I can't find it by searching for that phrase.
@JonB well that bug report definitely looks like it's exactly what I'm seeing. Unfortunately the comment thread isn't making much sense to me:
In case someone else does stumble over that odd behaviour and this comment: The trick is to open the new instance first and then close the old one when dealing with an instance of a popover view. It is a bit counterintuitive, but it works for me. I just tested this in 311014 and it works if you wait reasonable amount of time. Closing it in favour of #450 and #392. When these two issues will be done, you will know when exactly you can present it again.
How would one "open the new instance first, and then close the old one"? I don't really have control over when the popover is dismissed, and I don't want a new one to show until some time later, when the user taps the right place again. And the second comment...what is a "reasonable amount of time"? I've waited 30 seconds to a minute after closing the popover, and it still tracebacks.
Calling close on_main_thread seems like a good bet...do you mean that I wait some small amount of time after the view is closed, and then call close again on_main_thread?
Here's a bit of simple code which emulates what I'm doing in my more complicated code:
import ui class Popover(ui.View): def __init__(self, *args, **kws): ui.View.__init__(self, *args, **kws) self.label = ui.Label() self.label.text = "Popover" self.add_subview(self.label) def layout(self): self.label.x = 0 self.label.y = 0 self.label.width = self.width self.label.height = self.height def will_close(self): print "popover closing" class MyView(ui.View): def __init__(self, *args, **kws): ui.View.__init__(self, *args, **kws) self.popover = None self.button = ui.Button() self.button.title = "Show Popover" self.button.action = self.showPopover self.add_subview(self.button) def layout(self): self.button.x = 0 self.button.y = 0 self.button.width = self.width self.button.height = self.height def showPopover(self, sender): print "show popover" if self.popover is None: self.popover = Popover() self.popover.present(style="popover", popover_location=self.button.center) #self.popover.close() def run(): v = MyView() v.present('full_screen') if __name__ == '__main__': run()
First time I hit the "Show Popover" button, it shows. Second time, I get the traceback.
I have a small "options" UI which I want to display as a popover window when the user taps a certain place in the main interface.
It's working the first time I tap, the popover shows up in the place specified by popover_location in the call to view.present(). But after dismissing it the first time, trying to show it again produces this traceback:
Traceback (most recent call last): File "/private/var/mobile/Library/Mobile Documents.../Gestures.py", line 443, in _general_action action(data) File "/private/var/mobile/Library/Mobile Documents.../controls.py", line 1578, in _handleSingleTap picker.present(style="popover", animated=False, popover_location=(x,y)) ValueError: View is already being presented or animation is in progress
I removed everything from the ui.View subclass which defines the popover so it is an empty shell:
class Popover(ui.View): def __init__(self, func, *args, **kws): views.PyUiView.__init__(self, *args, **kws) print "popover initialized" def will_close(self): print "popover closing"
and I still get the traceback the second time I try to call present() from the code which handles displaying the view, or any time thereafter.
Is calling present() on the same view more than once not allowed (I don't see anything indicating that in the docs)? I'm not explicitly calling view.close(), but I thought that once the popover was dismissed, that was automatic, which is why I do see will_close() being called. Even if I attach a callback which is called from will_close() and tries calling view.close() on the popover from the outside, it still doesn't prevent the traceback.
Hey all, I was seeing if I could wrap the UIStepper control to use it in my UI, and I got this far:
class Stepper(ui.View): def __init__(self, *args, **kws): import objc_util self._stepper = objc_util.ObjCClass("UIStepper").new() objcview = objc_util.ObjCInstance(self) objcview.addSubview_(self._stepper) self._stepper.flex = "WH" def layout(self): self._stepper.height = self.height self._stepper.width = self.width
And it does show a UIStepper widget in my view. But...that setting of the flex attribute appears to do nothing, and the layout() method which tries to force the stepper to be the size of the parent view doesn't do anything either.
I think this has to do with the fact that the actual objc view which the stepper is added to is not really manipulated by my view's layout() method or the flex attribute...but I'm not clear on how it works.
I'm also curious how best to create a pass-through for all the properties of the UIStepper I'd like to be able to get/set: just write a pass-through property on my ui.View which sets the underlying property, or is there some clever way to override setattr and getattr to automatically pass through to an objc view?
@mikael thankfully it does not suffer from that strange vanishing-definitions thing. Though I am now locally importing objc_util anyplace I try to use ObjCInstance() or ObjCClass(), which appears to prevent it from showing up and causing trouble.
I tested the above technique using weakref to hold the reference to the Gestures object and it worked fine, so that will prevent a cyclic-reference:
self._delegate = PythonistaGestureDelegate.new() import weakref self._delegate._gestures = weakref.ref(self)
@mikael a little more fiddling around, and looking at how other people have used objc_util and ObjCInstance's, it seems I can just stash a reference to the Gestures object instance for my control in the actual delegate instance created for that control:
self._delegate = PythonistaGestureDelegate.new() self._delegate._gestures = self
and then I can safely access that reference in the objc callbacks:
def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_(_self, _sel, gr, other_gr): import objc_util delegate_instance = objc_util.ObjCInstance(_self) gestures_instance = delegate_instance._gestures return gestures_instance.objc_should_recognize_simultaneously(self.recognize_simultaneously, gr, other_gr)
so now only one actual delegate class definition is created in objective-c, and the delegate instances for each control are all separately able to access the Gestures instance they use. I'll have to make sure I break the cyclic reference between the gesture delegate instance and the Gestures instance, but I can do that via a weakref or explicitly removing the cycle when I get rid of the control.