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] Stop Widget View Refresh
-
Long time listener, first time caller. I apologize if this has been asked. I dug around the forum for thirty minutes. How can I make the widget view persistent? As it is now, whenever the widgets are brought up only Pythonista refreshes itself. My little weather widget only updates the interface on certain time intervals. Can I stop the widget window from needing to refresh? You can see what I’m taliking about in my link. Thanks for the help!
GIF view[Edit]
For further clarification in that GIF: On the first swipe over to the widgets the script updates itself and saves the data. The second scroll over to the widgets it’s just pulling from that stored data. -
Not sure if I'll help you ... IIRC Today Widget is problematic, doesn't work well and I think that Ole said that he is thinking about removing it. Maybe I'm wrong, but I think I read it when I was going through all issues on the GitHub. Just try to search them (even the closed ones), maybe you'll find something.
NotificationCenter.framework
Every time Notification Center appears, you have the opportunity to update widget content. It's done via widgetPerformUpdateWithCompletionHandler: and then you have to call completion handler with NCUpdateResult, which can be:
.NewData
- iOS should refresh widget,.NoData
- previous snapshot made by the system is still valid, no need to update it,.Failed
- something bad happens, exception, whatever.
Pythonista appex
There're two functions you should be interested in:
get_widget_view()
set_widget_view()
Not sure how
set_widget_view()
works internally, because it calls_appex.set_widget_view()
(have no source for_appex
). And it's also not documented how these two methods cope withNCUpdateResult
, what happens whenset_widget_view()
is not called, etc.We can just guess - and my guess is that it returns
.NewData
every single time which leads to widget refresh. Is there a way how to confirm this theory? Maybe.import ui import appex import datetime lbl = appex.get_widget_view() if not lbl: lbl = ui.Label(frame=(0, 0, 0, 44)) lbl.alignment = ui.ALIGN_CENTER lbl.background_color = 'black' lbl.text_color = 'white' appex.set_widget_view(lbl) lbl.text = str(datetime.datetime.now())
This simple widget gets current widget view, if it not exists, view is created and set by
set_widget_view()
. If it exists,set_widget_view()
is not called and we're just updatingtext
property. Every single time notification center appears, widget is updated - I see time updates. This kind of confirms my theory, because.NoData
shouldn't lead to refresh and old snapshot should be displayed.Ideal world
I would expect that every widget script should have function
perform_update
like this:def perform_update(widget_view) -> bool: # Just do your stuff, even set new view via `set_widget_view`, ... # Load data, do whatever you want to do # return True to simulate `.NewData` # return False to simulate `.NoData` # raise to simulate `.Failed`
Or something similar, so, you can simulate all these three states. Unfortunately, we have nothing like this.
Recommendation
- Do not call
set_widget_view
if your view was already set (check viaget_widget_view
&.name
property) - Do not update your widget view if it's not necessary (already displayed, not enough time elapsed, ...)
You can still experience some animations (Show More button appears / disappears), but it's not very frequent.
Example
Here's example widget. What it does?
- First time this widget is displayed, there's red background and current timestamp
- Widget updates timestamp only if at least 10s elapsed from the initial / last update
- When there's consequent update (10s elapsed & not initial display), background is set to black
It shoulda kinda simulate what you want. Just use different conditions, check if you have data or not, etc.
import time import appex import ui from objc_util import ObjCClass NSUserDefaults = ObjCClass('NSUserDefaults') class TimestampWidgetView(ui.View): NAME = 'TimestampWidgetView' # Can be whatever, but it should be unique per Pythonista & any script # Reversed domain is used followed by whatever you want _LAST_UPDATE_KEY = 'com.robertvojta.pythonista.widget.last_update_key' def __init__(self, update_interval=10): super().__init__(frame=(0, 0, 0, 44)) self.update_interval = update_interval self._defaults = None self.label = ui.Label(frame=self.bounds) self.label.flex = 'WH' self.label.background_color = 'red' self.label.text_color = 'white' self.label.alignment = ui.ALIGN_CENTER self.add_subview(self.label) # WidgetView is being initialized, we should update content self.update_content(force=True) @property def defaults(self): if not self._defaults: self._defaults = NSUserDefaults.standardUserDefaults() return self._defaults @property def last_update(self): return self.defaults.integerForKey_(self._LAST_UPDATE_KEY) @last_update.setter def last_update(self, value): self.defaults.setInteger_forKey_(value, self._LAST_UPDATE_KEY) def update_content(self, force=False): # Get the time of when the update was called # # NOTE: Casting to int, because setFloat_forKey_ and floatForKey_ on self.defaults # produces weird values timestamp = int(time.time()) if not force and timestamp - self.last_update < self.update_interval: # Not enough time elapsed and we're not forced (initialisation) to update content, just skip it return # Update content in whatever way self.label.text = str(timestamp) if not force: # If update wasn't forced, change the color to black, just to see the difference self.label.background_color = 'black' # Store the time, just for the next comparison self.last_update = timestamp widget_view = appex.get_widget_view() # Can't use isinstance(widget_view, TimestampWidgetView) here, just use .name property if widget_view and widget_view.name == TimestampWidgetView.NAME: widget_view.update_content() else: widget_view = TimestampWidgetView() widget_view.name = TimestampWidgetView.NAME appex.set_widget_view(widget_view)
As I wrote, wild guesses and experiments. Not enough documentation how it actually works and based on current
appex
API, there's no hope to make it better. -
@zrzka this is brilliant. Thank you tremendously for the very thorough reply. I will experiment with this on the weekend. This looks to be exactly what I’m looking for. Hopefully the widget view doesn’t get taken away again. Though it’s problematic and the memory constraints really limit it... I can do relatively functional things within those boundaries.
-
@zrzka, below is a butchered version of your code. But I thought you or someone else here could make your code more re-usable, i tried though subclassing. I had a crude attempt at it just to get the meaning across. But It would be nice to have a recipe to make a well behaved Today Widget.
I know there are issues with what I have done. I am sure this could be simplied a lot as well as being written correctly. But I can see something like this you save as a snippet with a subclassed stub, and you have the mechanics for a well behaved Today Widget.import time import appex import ui from objc_util import ObjCClass NSUserDefaults = ObjCClass('NSUserDefaults') _MY_UPDATE_KEY = 'com.phuket2.pythonista.widget.last_update_key' _WIDGET_NAME = 'My Widget' class TodayWidgetBase(ui.View): NAME = _WIDGET_NAME # Can be whatever, but it should be unique per Pythonista & any script # Reversed domain is used followed by whatever you want _LAST_UPDATE_KEY = _MY_UPDATE_KEY def __init__(self, update_interval=10, **kwargs): super().__init__(self, **kwargs) self.update_interval = update_interval self._defaults = None # WidgetView is being initialized, we should update content self.update_content(force=True) @property def defaults(self): if not self._defaults: self._defaults = NSUserDefaults.standardUserDefaults() return self._defaults @property def last_update(self): return self.defaults.integerForKey_(self._LAST_UPDATE_KEY) @last_update.setter def last_update(self, value): self.defaults.setInteger_forKey_(value, self._LAST_UPDATE_KEY) def update(self): self.update_content() def update_content(self, force=False): # Get the time of when the update was called # # NOTE: Casting to int, because setFloat_forKey_ and floatForKey_ on self.defaults # produces weird values timestamp = int(time.time()) if not force and timestamp - self.last_update < self.update_interval: # Not enough time elapsed and we're not forced (initialisation) to update content, just skip it return # Update content in whatever way self.update_widget() if not force: # If update wasn't forced self.update_widget() # Store the time, just for the next comparison self.last_update = timestamp class MyTodayWidget(TodayWidgetBase): def __init__(self, update_interval=10, **kwargs): self.start_time = time.time() self.lb = None self.make_view() super().__init__(update_interval, **kwargs) def make_view(self): lb = ui.Label(frame=self.bounds, flex='WH', alignment=ui.ALIGN_CENTER) self.lb = lb self.add_subview(lb) def update_widget(self): self.lb.text = str(time.time() - self.start_time) # just for testing...After code correct, _PREVIEW should be False so you can # run your code to setup persitent data etc... _PREVIEW = True if not appex.is_running_extension() and not _PREVIEW: print('here we can set up what we need to do, write to a database etc...') else: # note if h <= 120 you will not see the 'show more' button in the menu bar # at least thats what i can see! f = (0, 0, 500, 220) update_interval = 1 widget_view = appex.get_widget_view() # Can't use isinstance(widget_view, TimestampWidgetView) here, just use .name property if widget_view and widget_view.name == MyTodayWidget.NAME: widget_view.update_content() else: widget_view = MyTodayWidget(update_interval, frame=f) widget_view.name = MyTodayWidget.NAME appex.set_widget_view(widget_view) ```python
-
@Phuket2 well, it depends on how you do define reusability. From my point of view, it's a code I can put into separate module, import it and reuse it somehow. The problem with your one is that it's not reusable from this point of view. Here's one:
import time import appex import ui from objc_util import ObjCClass NSUserDefaults = ObjCClass('NSUserDefaults') class UpdateIntervalWidgetView(ui.View): """Today widget view allowing to update content if at least specific time elapses. You have to override `prepare_content` and `update_content` methods. Optionally, you can override `PREFIX` attribute if you'd like to use different prefix for NSUserDefaults keys. Args: name (str): Widget name used for comparison. Use something unique. update_interval (int): Time interval in seconds which must elapse before next update is performed. height (int): Widget height. """ PREFIX = 'com.robertvojta.pythonista.today_widget.last_update' def __init__(self, name, update_interval, height): super().__init__(frame=(0, 0, 0, height)) self.name = name self._update_interval = update_interval self.__defaults = None self._last_update_key = f'{self.PREFIX}.{name}' self.prepare_content() self.update(True) def prepare_content(self): """Override to build widget view controls hierarchy with no data. .. note:: `update_content` is called immediately after this method finishes. """ raise NotImplementedError('Override and build your view hierarchy here') def update_content(self): """Override and update widget view content.""" raise NotImplementedError('Override and update widget view hierarchy') @property def _defaults(self): if not self.__defaults: self.__defaults = NSUserDefaults.standardUserDefaults() return self.__defaults @property def _last_update(self): return self._defaults.integerForKey_(self._last_update_key) @_last_update.setter def _last_update(self, value): self._defaults.setInteger_forKey_(value, self._last_update_key) def update(self, force=False): timestamp = int(time.time()) if not force and timestamp - self._last_update < self._update_interval: return self.update_content() self._last_update = timestamp class TimeWidgetView(UpdateIntervalWidgetView): def prepare_content(self): self.label = ui.Label(frame=self.bounds) self.label.flex = 'WH' self.label.background_color = 'black' self.label.text_color = 'white' self.label.alignment = ui.ALIGN_CENTER self.add_subview(self.label) def update_content(self): self.label.text = str(int(time.time())) widget_view = appex.get_widget_view() if widget_view and widget_view.name == 'MyWidget': widget_view.update() else: widget_view = TimeWidgetView('MyWidget', 1, 33) appex.set_widget_view(widget_view)
Everything above
TimeWidgetView
can be placed into separate module. -
@Dann you're welcome, hope it helps, not sure, but we will see :)
-
@zrzka , ok maybe use ability was the wrong term. But still my meaning is the same. Move as much into the base class as possible. For example can some of the main be moved into the base class?
I dont know this call/api NSUserDefaults = ObjCClass('NSUserDefaults'). But I can guess what its doing. Similar to a registry call, i am guessing. But if it was replace with a python call to say to keychain or to read/write a value from dict on disk, would this be a big impediment to memory and for speed? Or maybe another pure python call, might be equivalent. I just ask because if its pure python, people have a better chance to debug. For example, your code you posted does not call update method more than the first call in the base class init.Also, I think a base class like you have done could support most peoples needs for a TodayWidget. Static or dynamic data.
Also, maybe the base class could take a param to a setup func/class and check the appex.is_running_extension(). So you can present a interface to populate whatever values your TW requires.
I know this may seem trivial too you(I can understand that). But for a lot of us its not trivial. To have a very nice base class , from another module or in the same module could be so helpful. I am not suggesting you need to be the one that writes it. But in my mind a well crafted base class could trivialise building well behaved TodayWidgets for users like me with limited ability. -
For example can some of the main be moved into the base class?
Can be moved.
I dont know this call/api NSUserDefaults = ObjCClass('NSUserDefaults'). But I can guess what its doing. Similar to a registry call, i am guessing.
Don't guess and use Google. NSUserDefaults.
But if it was replace with a python call to say to keychain or to read/write a value from dict on disk, would this be a big impediment to memory and for speed? Or maybe another pure python call, might be equivalent. I just ask because if its pure python, people have a better chance to debug.
Feel free to replace NSUserDefaults with whatever you want. It's a simple storage of
int
. Don't usekeychain
for this.For example, your code you posted does not call update method more than the first call in the base class init.
It does,
widget_view.update()
at the end. It's not a widget which updates content on a regular basis (like every Xs), but only when the update is requested by the system and at least Xs elapsed from the last update. You should read links I did include in one of my previous replies about Today Widget life cycle, how it does work, read Pythonista documentation about Today Widget (that the main is called every single time), ...Also, I think a base class like you have done could support most peoples needs for a TodayWidget. Static or dynamic data. Also, maybe the base class could take a param to a setup func/class and check the appex.is_running_extension(). So you can present a interface to populate whatever values your TW requires.
Feel free to modify it.
I know this may seem trivial too you(I can understand that). But for a lot of us its not trivial. To have a very nice base class , from another module or in the same module could be so helpful. I am not suggesting you need to be the one that writes it. But in my mind a well crafted base class could trivialise building well behaved TodayWidgets for users like me with limited ability.
Look, I don't use Today Widget and I only tried to help @Dann. I'm not going to write generic today widget class, because it has no sense. You can't simply write a class which can help everyeone. I'm also not a big fan of virtual reusability = design something reusable you don't need. I don't write reusable classes / functions / modules / ... unless I need them at least in three places, ... Otherwise it's time wasting, because you don't know what you're gonna need.
-
@zrzka , lol your are a hard man :) I am joking. Look, i get your point. But i did mention in my post, I didn't expect you to write it. But I still think my post was ok, because maybe someone else out there reading this thread might think its worth doing. If not, then its also ok.
The reason I say i was guessing about NSUserDefaults, and i was guessing , was I am not good enough anyway to write what I think would be good enough. It's frustrating, but thats just my reality.
Again, i respect/understand your stance. You add a lot of value to Pythonista. That's the bottom line!
-
lol your are a hard man :) I am joking.
Maybe, I just publicly say what I think :) Kinda rare these days in our hyper correct world ...
But I still think my post was ok, because maybe someone else out there reading this thread might think its worth doing. If not, then its also ok.
Didn't say it wasn't ok :)
The reason I say i was guessing about NSUserDefaults, and i was guessing , was I am not good enough anyway to write what I think would be good enough. It's frustrating, but thats just my reality.
Don't underestimate yourself. Reality can be changed. Try it, post it, people can help, ...
An year ago, I didn't know a thing about AWS for example. Now we have a complete production on AWS utilising lot of services (Dynamo, RDS, Gateway, Lambda, VPC, EC2, EC2, ELB, SQS, SNS, ...). Spent last year in documentation, testing, solving fuckups, ... It was hard, really, but I'm pretty happy now.
Why I'm mentioning AWS. You decided to go with Pythonista, Python and iOS. Spend some time and try to learn ObjC, Apple frameworks and don't expect that Ole will produce module for every available framework covering all functionality - not maintainable. Basically, you can do almost everything without Ole's help. You'll learn something new, maybe in a slow way, but it's better than sitting, waiting and hoping. It's never late to start.
I started with iOS 10 years ago (March, 2008 when the first SDK was released). 9 years of extensive iOS apps development, slowed down little bit last year because of AWS. I started with Mac even sooner. That's the reason why I know a lot about iOS, not a lot about Python, just composing pieces of my knowledge together.
You shouldn't compare yourself with others or me at all, you shouldn't feel ashamed to ask, you just need to start with something and finish it.
You add a lot of value to Pythonista. That's the bottom line!
I don't think so :)
-
@zrzka , well you do add a lot here both with your answers and BlackMamba. That's me not be being shy and trying to be politically correct. Don't worry, I am not worried about speaking my mind also. But we all have different thresholds of that. I comment in this forum fairy freely, but I also try to be encouraging whenever i can. But on social media, I have stopped almost 100%. It's just not worth it. For me its a waste of time and full of trolls. Besides that, when you live outside your own country as i do, if you to be very aware of what is ok to say and what's is not. Won't go into details, but you can find yourself in big problems very quickly. Anyway, enough of that.
Yeah, I get your meaning about learning Objc etc... But I am just not in the right mindset. I am on a Python hobbiest journey. If i was younger, I am sure I would go down that track. I did that once already with the Mac Toolbox when macs were running on Motorola 68k series CPU's.But i will start to look at the Apple docs a bit more. I had some issues today about the documentation of the ui.ActivityIndicator, I looked up the apple docs. It was enlightening but at the say time raised my questions in my mind than answers :).
Anyway, I will just continue on at my slow pace. I still get a buzz out of it.