omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    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

    Pythonista
    3
    11
    6837
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • zrzka
      zrzka last edited by zrzka

      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.

      Dann Phuket2 2 Replies Last reply Reply Quote 2
      • Dann
        Dann @zrzka last edited by

        @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.

        1 Reply Last reply Reply Quote 0
        • Phuket2
          Phuket2 @zrzka last edited by

          @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
          1 Reply Last reply Reply Quote 1
          • zrzka
            zrzka last edited by

            @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.

            Phuket2 1 Reply Last reply Reply Quote 0
            • zrzka
              zrzka last edited by

              @Dann you're welcome, hope it helps, not sure, but we will see :)

              1 Reply Last reply Reply Quote 0
              • Phuket2
                Phuket2 @zrzka last edited by

                @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.

                1 Reply Last reply Reply Quote 0
                • zrzka
                  zrzka last edited by

                  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 use keychain 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.

                  Phuket2 1 Reply Last reply Reply Quote 1
                  • Phuket2
                    Phuket2 @zrzka last edited by

                    @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!

                    1 Reply Last reply Reply Quote 1
                    • zrzka
                      zrzka last edited by

                      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 :)

                      Phuket2 1 Reply Last reply Reply Quote 0
                      • Phuket2
                        Phuket2 @zrzka last edited by

                        @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.

                        1 Reply Last reply Reply Quote 0
                        • First post
                          Last post
                        Powered by NodeBB Forums | Contributors