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.
Trying to figure out an appex crash
-
I've got a little script for making half-size screenshots that I'd like to use as an appex. When I invoke the script normally, it pops up a photo selector and everything works fine. If I go into my screenshots album, select some photos, and then try to invoke the script via the appex mechanism, it processes a few photos (typically 3-6) and then the Pythonista window closes with no further indication of what has happened. It's not deterministic; if I delete the partial results and go back and try it again with the exact same photos, sometimes it will work. Here's a little YouTube video showing what happens. There's no information left the the console unfortunately. I also tried enabling the faulthandler, but there's nothing written to the file that I give it. I've added various tweaks thinking that maybe there was some kind of race condition, things like writing the PNG files to separate filenames or putting a one second sleep at various spots. None of those made any difference. I'm at a bit of a loss right now, so any suggestions for what might be wrong or how to proceed in debugging would be welcome.
import photos import appex import datetime from PIL import Image def main(): if appex.is_running_extension(): imgs=appex.get_images() else: screenshots=photos.get_screenshots_album() assets=photos.pick_asset(assets=screenshots, title='Select screenshots to shrink', multi=True) if assets: imgs=[a.get_image() for a in assets] else: imgs=[] if not imgs: print('No input screenshots') return album_name='Small Screenshots' albums=[a for a in photos.get_albums() if a.title == album_name] if albums: album=albums[0] else: album=photos.create_album(album_name) for (i, img) in enumerate(imgs): print(f'Shrinking screenshot number {i+1}...') size=img.size small_img=img.resize((size[0]//2, size[1]//2), Image.BILINEAR) filename='.temp.png' with open(filename, 'wb') as f: small_img.save(f, 'PNG') asset=photos.create_image_asset(filename) album.add_assets([asset]) asset.creation_date=datetime.datetime.now() print('Done!') if __name__ == '__main__': main()
-
Do you have Mac & Xcode access? If so, you can access your device logs (realtime). Connect your device, reveal logs and try to run your extension. Anything suspicious? Just thinking aloud, because of Application Extension documentation quote (section Optimize Efficiency and Performance):
App extensions should feel nimble and lightweight to users. Design your app extension to launch quickly, aiming for well under one second. An extension that launches too slowly is terminated by the system.
Memory limits for running app extensions are significantly lower than the memory limits imposed on a foreground app. On both platforms, the system may aggressively terminate extensions because users want to return to their main goal in the host app. Some extensions may have lower memory limits than others: For example, widgets must be especially efficient because users are likely to have several widgets open at the same time.
Your app extension doesn’t own the main run loop, so it’s crucial that you follow the established rules for good behavior in main run loops. For example, if your extension blocks the main run loop, it can create a bad user experience in another extension or app.
Keep in mind that the GPU is a shared resource in the system. App extensions do not get top priority for shared resources; for example, a Today widget that runs a graphics-intensive game might give users a bad experience. The system is likely to terminate such an extension because of memory pressure. Functionality that makes heavy use of system resources is appropriate for an app, not an app extension.
It can explain why your script works normally outside of application extension context.
-
Here's slightly optimised version, which allows me to process even 50 images at once on iPhone SE / iPad Pro. Still room for an improvement. Anyway, I think, that you script was killed by the system, because you hold multiple versions of images in memory. Time to rewrite
get_attachments
,get_images
, ... as generators?import photos import appex import datetime import os from PIL import Image def ensure_album(name): albums = [x for x in photos.get_albums() if x.title == name] if albums: return albums[0] print(f'Going to create {name} album') return photos.create_album(name) def appex_images(): attachments = appex.get_attachments('public.image') for x in attachments: image = appex._image_from_attachment(x) if image: yield image def assets_images(assets): if assets: for x in assets: yield x.get_image() def main(): if appex.is_running_extension(): images = appex_images() else: album = photos.get_screenshots_album() assets = photos.pick_asset( assets=album, title='Select screenshots to shrink', multi=True ) images = assets_images(assets) album = None for image in images: if not album: album = ensure_album('Small screenshots') small_size = tuple(x // 2 for x in image.size) print(f'Resizing image of size {image.size} to {small_size}') small_image = image.resize(small_size, Image.BILINEAR) print('Saving image to temporary file') with open('.tmp.png', 'wb') as f: small_image.save(f, 'PNG') print('Creating asset from temporary file') asset = photos.create_image_asset('.tmp.png') album.add_assets([asset]) asset.creation_date = datetime.datetime.now() try: os.remove('.tmp.png') except FileNotFoundError: pass print('Done') if __name__ == '__main__': main()
-
The appex resource limits would explain everything. I don’t have Xcode access to check, but it makes sense. Thanks for helping out a Pythonista newbie!