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 with NCUpdateResult, what happens when set_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 updating text 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 via get_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.