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
    6857
    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.
    • Dann
      Dann last edited by Dann

      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.

      1 Reply Last reply Reply Quote 0
      • 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