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.
Unable to use ObjCBlock in pythonista3.4
-
Hey all you pythonistas, it's been a while.
Super super excited about the new version of Pythonista!
I'm converting the big project we have which was written in python2/pythonista3.3 to be python3/pythonista3.4, and I ran into one major issue: I'm getting crashes if I try to use ObjCBlocks in pythonista3.4 which worked fine in pythonista3.3.Here's the main example, pulled from a much more complex chunk of code, of a custom sheet presentation controller, which is supposed to be able to call a completion function via a block:
import objc_util import ui UIViewController = objc_util.ObjCClass("UIViewController") UIView = objc_util.ObjCClass("UIView") UIModalPresentationFormSheet = 2 UIModalTransitionStyleCoverVertical = 0 def getRootUIViewController(): app = objc_util.UIApplication.sharedApplication() window = app.delegate().window() if not window: return None return window.rootViewController() def getUIViewController(view): #### If the view is a standard pythonista ui.View, we can #### try to directly retrieve the view controller SUIView = objc_util.ObjCClass('SUIView') SUIViewController = objc_util.ObjCClass('SUIViewController') UIView = objc_util.ObjCClass('UIView') UIViewController = objc_util.ObjCClass('UIViewController') if isinstance(view, ui.View): viewobj = view.objc_instance vc = SUIViewController.viewControllerForView_(viewobj) if vc is not None: return vc elif isinstance(view, objc_util.ObjCInstance) and \ view.isKindOfClass_(SUIView): viewobj = view vc = SUIViewController.viewControllerForView_(viewobj) if vc is not None: return vc #### Otherwise we have to search the "responder chain" #### for the first instance of a UIViewController elif isinstance(view, objc_util.ObjCInstance) and \ (view.isKindOfClass_(UIView) or view.isKindOfClass_(UIViewController)): viewobj = view viewResponder = viewobj.nextResponder() try: while not viewResponder.isKindOfClass_(UIViewController): viewResponder = viewResponder.nextResponder() except AttributeError: return None return viewResponder def presentAsSheet(view, sourceView=None, whenPresented=None): #### create a new modal view controller and set its presentation #### style to the sheet style, and size it to the content currentvc = getUIViewController(view) if currentvc is not None: currentvc.view = None sheetController = UIViewController.new().autorelease() sheetController.view = view.objc_instance sheetController.modalPresentationStyle = UIModalPresentationFormSheet sheetController.modalTransitionStyle = \ UIModalTransitionStyleCoverVertical sheetController.preferredContentSize = \ objc_util.CGSize(view.width, view.height) #### retrieve the presentation controller for the new modal #### view controller, bail if that fails for some reason presentationController = \ sheetController.presentationController() if presentationController is None: print("Unable to get presentation controller to present.") return #### find the view controller which is presenting the source view, #### that view controller will present the sheet overlay if sourceView is not None: sourcevc = getUIViewController(sourceView) else: sourcevc = getRootUIViewController() if sourcevc is None: print("Unable to find view controller for source view.") return #### create a block to call the presentation completion _block = None if whenPresented: _block = objc_util.ObjCBlock( whenPresented, restype=None, argtypes=[] ) #### present the view controller containing the content view #### via the sheet presentation controller sourcevc.presentViewController_animated_completion_( sheetController, True, _block) main = None def whenPresented(): print("PRESENTATION COMPLETE") def presentSheet(sender): print("PRESENT SHEET:", sender) sheet = ui.View() sheet.frame = (0,0,300,200) sheet.background_color = (1.0,1.0,1.0,1.0) l = ui.Label() l.text = "Sheet Presented View" l.text_color = (0,0,0,1.0) l.alignment = ui.ALIGN_CENTER l.frame = (0,0,300,200) sheet.add_subview(l) #### presenting with a completion function crashes! presentAsSheet(sheet, main, whenPresented=whenPresented) #### presenting without completion function works fine... #presentAsSheet(sheet, main, whenPresented=None) main = ui.View() main.background_color = (1.0,1.0,1.0,1.0) main.frame = (0,0,500,500) b = ui.Button() b.frame = (10,10,200,40) b.title = "Present Sheet" b.action = presentSheet main.add_subview(b) main.present("fullscreen")
I can't get the block to run without a segmentation fault.
presentViewController_animated_completion_(...)
is defined here:
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621380-presentviewcontrollerAnd it says it has no return and takes no parameters...so I'm not sure where I've gone wrong. Has anyone been successfully using blocks in python3/pythonista3.4?
-
@shinyformica you're right, even this small script using block crashes with segmentation error
# coding: utf-8 from objc_util import ObjCInstance, ObjCClass, ObjCBlock, c_void_p def handler(_cmd, _data, _error): print(ObjCInstance(_data)) handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p, c_void_p, c_void_p]) def main(): CMAltimeter = ObjCClass('CMAltimeter') NSOperationQueue = ObjCClass('NSOperationQueue') if not CMAltimeter.isRelativeAltitudeAvailable(): print('This device has no barometer.') return altimeter = CMAltimeter.new() main_q = NSOperationQueue.mainQueue() altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler_block) print('Started altitude updates.') try: while True: pass finally: altimeter.stopRelativeAltitudeUpdates() print('Updates stopped.') if __name__ == '__main__': main()
-
@shinyformica I have another script where an ObjcBlock works correctly. This, the problem should occur with presentViewController_animated_completion_ with a block, I guess
-
@cvp so...you do have an example where ObjCBlock works without a crash? I definitely would want to look at an example that still works and try to discern what has changed.
-
@shinyformica Oups, sorry. I thought I had a working example but... The program works until I tap a button where action uses block. I did not check correctly. I apologize.
-
@cvp well darn, seems like ObjCBlock is genuinely broken, then? That's...not ideal. We use blocks in a quite a few places where our code has to interact with the objective-c side of things. So this situation basically bricks our code with pythonista3.4.
I wonder what has changed? Anyone out there (maybe @JonB or even @omz) have any thoughts?
-
@shinyformica Don't forget that Pythonista doc says "Warning
Block support is experimental. If you have the alternative of using an API that doesn’t require blocks, it is strongly recommended that you do so." -
@shinyformica I wonder if you need to hang onto the reference to _block? I know some things have changed timing wise, the function might be exiting faster than that objc callback uses the block.
I wonder if it would make sense to switch over to rubicon -- IIRC there are some features that make using objc much less error prone.
Does the simple example in the docs for ObjCBlock still work?
(Never mind... @cvp example uses a global and still a problem)
I can try playing around with it a bit. IIRC, back when objc was first introduced in pythonista2, we had to do a lot of manual block structure definition and CFUNCTYPE stuff to define blocks. Might go back and look at those early threads to go back to basics sort of speak.
-
-
@shinyformica this script with ObjcBlock works
from objc_util import * from time import sleep def Country_reverseGeocodeLocation(lat,lon): global handler_done,ISOcountryCode handler_done = None ISOcountryCode = None def handler(_cmd,obj1_ptr,_error): global handler_done,ISOcountryCode print('handler called') try: if not _error and obj1_ptr: obj1 = ObjCInstance(obj1_ptr) for CLPlacemark in obj1: ISOcountryCode = str(CLPlacemark.ISOcountryCode()) if ISOcountryCode == 'None': # special case of Market Reef island which belongs to Sweden and Finland, # and Apple reverseGeocodeLocation does not return an ISOcountryCode # check if location name = Bottenhavet (see of island), simulate # returns Finland. Market Reef entity will be identified later #print(str(CLPlacemark.name())) if str(CLPlacemark.name()) == 'Bottenhavet': ISOcountryCode = 'FI' except Exception as e: print('error',e) handler_done = True return CLGeocoder = ObjCClass('CLGeocoder').alloc().init() handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p, c_void_p, c_void_p]) CLLocation = ObjCClass('CLLocation').alloc().initWithLatitude_longitude_(lat,lon) CLGeocoder.reverseGeocodeLocation_completionHandler_(CLLocation, handler_block) # wait handler called and finished while not handler_done: sleep(0.01) return ISOcountryCode lat,lon = (-51.6500051, -58.4924793) print(Country_reverseGeocodeLocation(lat,lon))
-
@cvp oh! Fantastic, thanks for finding that. Interesting...have to try and see why that formulation is working. Good to have a working example.
-
@shinyformica I have to say that I have tested ten scripts I had before I find this one...
-
@cvp possibly just luck that this particular script works. But maybe there's something about the way it is set up. I had not found a working example so far in any of the ways we had been using ObjCBlock, and I'd also tried a couple of examples in the forum, like that barometer one.
-
@shinyformica Perhaps this use of wait/sleep
-
@shinyformica your, modified, script does not crash but hangs, like if the handler was never called
import objc_util import ui from time import sleep UIViewController = objc_util.ObjCClass("UIViewController") UIView = objc_util.ObjCClass("UIView") UIModalPresentationFormSheet = 2 UIModalTransitionStyleCoverVertical = 0 def getRootUIViewController(): app = objc_util.UIApplication.sharedApplication() window = app.delegate().window() if not window: return None return window.rootViewController() def getUIViewController(view): #### If the view is a standard pythonista ui.View, we can #### try to directly retrieve the view controller SUIView = objc_util.ObjCClass('SUIView') SUIViewController = objc_util.ObjCClass('SUIViewController') UIView = objc_util.ObjCClass('UIView') UIViewController = objc_util.ObjCClass('UIViewController') if isinstance(view, ui.View): viewobj = view.objc_instance vc = SUIViewController.viewControllerForView_(viewobj) if vc is not None: return vc elif isinstance(view, objc_util.ObjCInstance) and \ view.isKindOfClass_(SUIView): viewobj = view vc = SUIViewController.viewControllerForView_(viewobj) if vc is not None: return vc #### Otherwise we have to search the "responder chain" #### for the first instance of a UIViewController elif isinstance(view, objc_util.ObjCInstance) and \ (view.isKindOfClass_(UIView) or view.isKindOfClass_(UIViewController)): viewobj = view viewResponder = viewobj.nextResponder() try: while not viewResponder.isKindOfClass_(UIViewController): viewResponder = viewResponder.nextResponder() except AttributeError: return None return viewResponder def presentAsSheet(view, sourceView=None, whenPresented=None): #### create a new modal view controller and set its presentation #### style to the sheet style, and size it to the content currentvc = getUIViewController(view) if currentvc is not None: currentvc.view = None sheetController = UIViewController.new().autorelease() sheetController.view = view.objc_instance sheetController.modalPresentationStyle = UIModalPresentationFormSheet sheetController.modalTransitionStyle = \ UIModalTransitionStyleCoverVertical sheetController.preferredContentSize = \ objc_util.CGSize(view.width, view.height) #### retrieve the presentation controller for the new modal #### view controller, bail if that fails for some reason presentationController = \ sheetController.presentationController() if presentationController is None: print("Unable to get presentation controller to present.") return #### find the view controller which is presenting the source view, #### that view controller will present the sheet overlay if sourceView is not None: sourcevc = getUIViewController(sourceView) else: sourcevc = getRootUIViewController() if sourcevc is None: print("Unable to find view controller for source view.") return #### create a block to call the presentation completion _block = None if whenPresented: _block = objc_util.ObjCBlock( whenPresented, restype=None, argtypes=[] ) #### present the view controller containing the content view #### via the sheet presentation controller sourcevc.presentViewController_animated_completion_(sheetController, True, _block) main = None def whenPresented(): global handler_done print("PRESENTATION COMPLETE") handler_done = True def presentSheet(sender): global handler_done handler_done = None print("PRESENT SHEET:", sender) sheet = ui.View() sheet.frame = (0,0,300,200) sheet.background_color = (1.0,1.0,1.0,1.0) l = ui.Label() l.text = "Sheet Presented View" l.text_color = (0,0,0,1.0) l.alignment = ui.ALIGN_CENTER l.frame = (0,0,300,200) sheet.add_subview(l) #### presenting with a completion function crashes! presentAsSheet(sheet, main, whenPresented=whenPresented) # wait handler called and finished while not handler_done: sleep(0.01) #### presenting without completion function works fine... #presentAsSheet(sheet, main, whenPresented=None) main = ui.View() main.background_color = (1.0,1.0,1.0,1.0) main.frame = (0,0,500,500) b = ui.Button() b.frame = (10,10,200,40) b.title = "Present Sheet" b.action = presentSheet main.add_subview(b) main.present("fullscreen")
-
@cvp doesn't seem to be the
time.sleep()
that allows the reverse geocode example to work...if I strip that code down a bit, and remove the sleep, it still works without crashing:import objc_util import time handler_done = None country_code = None def handler(_cmd, placemarks, error): global country_code global handler_done print('handler called') if not error and placemarks: placemarks = objc_util.ObjCInstance(placemarks) for placemark in placemarks: country_code = str(placemark.ISOcountryCode()) handler_done = True handler_block = objc_util.ObjCBlock( handler, restype=None, argtypes=[objc_util.c_void_p, objc_util.c_void_p, objc_util.c_void_p] ) lat, lon = (37.7749, -122.4194) CLGeocoder = objc_util.ObjCClass('CLGeocoder').alloc().init() CLLocation = objc_util.ObjCClass('CLLocation').alloc().initWithLatitude_longitude_(lat,lon) CLGeocoder.reverseGeocodeLocation_completionHandler_(CLLocation, handler_block) # wait handler called and finished while not handler_done: #time.sleep(0) pass print("Country Code:", country_code)
So, something else about that particular block call works.
-
And if I make the barometer example nearly identical in structure, it still fails, even with a
time.sleep()
put in:import objc_util import time pressure = None def handler(_cmd, data, error): global pressure print("handler called") pressure = objc_util.ObjCInstance(data).pressure() handler_block = objc_util.ObjCBlock( handler, restype=None, argtypes=[objc_util.c_void_p, objc_util.c_void_p, objc_util.c_void_p] ) CMAltimeter = objc_util.ObjCClass('CMAltimeter') NSOperationQueue = objc_util.ObjCClass('NSOperationQueue') if not CMAltimeter.isRelativeAltitudeAvailable(): print('This device has no barometer.') else: altimeter = CMAltimeter.new() mainq = NSOperationQueue.mainQueue() altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(mainq, handler_block) try: while pressure is None: time.sleep(0) finally: altimeter.stopRelativeAltitudeUpdates() print("got pressure:", pressure)
so something else is going on here.
Must be something more fundamental in how the blocks are defined. -
One more datapoint. As @JonB suggested, I tried the example for
ObjCBlock
in the pythonista docs for theobjc_util
module:import objc_util cheeses = objc_util.ns(['Camembert', 'Feta', 'Gorgonzola']) print(cheeses) def compare(_cmd, a, b): a = objc_util.ObjCInstance(a).length() b = objc_util.ObjCInstance(b).length() if a > b: return 1 if a < b: return -1 return 0 # Note: The first (hidden) argument `_cmd` is the block itself, so there are three arguments instead of two. compare_block = objc_util.ObjCBlock( compare, restype=objc_util.NSInteger, argtypes=[objc_util.c_void_p, objc_util.c_void_p, objc_util.c_void_p]) sorted_cheeses = cheeses.sortedArrayUsingComparator_(compare_block) print(sorted_cheeses)
works fine, no crashes. So, that makes it seem even more likely that this has to do with the internals of how the block is created/presented to objective-c via CFFI.
-
Looks like rubicon may be the answer. Here's the same barometer example that crashes using
objc_util
converted to use rubicon:import sys import rubicon_objc import time pressure = None def handler(data, error): global pressure data = rubicon_objc.api.ObjCInstance(data) error = rubicon_objc.api.ObjCInstance(error) print("handler called:", data, error) pressure = data.pressure.floatValue handler_block = rubicon_objc.api.Block( handler, None, rubicon_objc.types.c_void_p, rubicon_objc.types.c_void_p ) CMAltimeter = rubicon_objc.api.ObjCClass('CMAltimeter') NSOperationQueue = rubicon_objc.api.ObjCClass('NSOperationQueue') if not CMAltimeter.isRelativeAltitudeAvailable(): print('This device has no barometer.') else: altimeter = CMAltimeter.new() mainq = NSOperationQueue.mainQueue print("Altimeter:", altimeter) print("Main Queue:", mainq) altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(mainq, handler_block) try: while pressure is None: pass finally: altimeter.stopRelativeAltitudeUpdates() print("got pressure:", pressure)
this doesn't crash. I don't know rubicon particularly well, so I don't know if I'm using it exactly as intended, but I'll see if it succeeds in other cases where objc_util crashes.
-
@shinyformica said
doesn't seem to be the time.sleep() that allows the reverse geocode example to work...if I strip that code down a bit, and remove the sleep, it still works without crashing:
I'm sure that some years ago, I have had to add this wait loop to get this code working. But ok, it is included in a very big script and was running in a separate thread.