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.


    UIPageControl (Paging scrollview with page indicators)

    Pythonista
    objcutil objc code-sharing
    5
    47
    22503
    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.
    • stephen
      stephen last edited by

      This post is deleted!
      1 Reply Last reply Reply Quote 0
      • stephen
        stephen @mikael last edited by

        This post is deleted!
        1 Reply Last reply Reply Quote 0
        • stephen
          stephen last edited by stephen

          @mikael @Samer

          For some reason self.scrollView.y is 20pt too high and it fails to flex H i set frame and flex and issue was corrected. setting y to 19 still drops butbit is hard to see.

          self.scrollView = ui.ScrollView(
          			delegate=self,
          			paging_enabled=True,
          			shows_horizontal_scroll_indicator=False,
          			bounces=False,
          			frame=(0, 20, w, h), flex='h',)
          
          1 Reply Last reply Reply Quote 0
          • Samer
            Samer last edited by

            @mikael huh, thats very strange I get the same effect using your example on my IPad Pro 11” (2018). It seems like initially the view overlaps with the info bar at the top of the screen (time, wifi, battery, etc) and upon touching the screen the view shifts down to not overlap with it. It’s hard to tell in your example due to the black text on black background. However if you set hide_title_bar to true in the built in example a similar effect happens. I will look into a fix in the next day or two.

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

              @mikael @Samer ill Give a more indepth respone 🤓

              this is complete Script with comments and just commenting out changes instead if removing.

              
              import ui
              from objc_util import ObjCClass, CGRect, create_objc_class, ObjCInstance, UIColor
              
              
              UIPageControl = ObjCClass('UIPageControl')
              
              
              def changePage(_self, _cmd):
              	self = ObjCInstance(_self)
              	self.page_control.set_page(self.page_control.pageControl.currentPage())
              
              
              ChangePageClass = create_objc_class("ChangePageClass", methods=[changePage])
              
              
              class PageControl(ui.View):
              	def __init__(self, **kwargs):
              		
              		
              
              		self.scrollView = ui.ScrollView(
              			delegate=self,
              			paging_enabled=True,
              			shows_horizontal_scroll_indicator=False,
              			bounces=False,
              			frame=self.bounds,
              			flex='WH',
              		)
              
              		self.pageControl = UIPageControl.alloc().init().autorelease()
              		self._target = ChangePageClass.new().autorelease()
              		self._target.page_control = self
              		self.pageControl.addTarget_action_forControlEvents_(self._target, 'changePage', 1 << 12)  #1<<12 = 4096
              		self.pageControl.numberOfPages = len(self.scrollView.subviews)
              		self.pageControl.currentPage = 0
              		self.pageControl.hidesForSinglePage = True
              	
              		self._prev_page = 0
              
              		super().add_subview(self.scrollView)
              		ObjCInstance(self).addSubview_(self.pageControl)
              
              		super().__init__(**kwargs)
              
              	def layout(self):
              		self.scrollView.content_size = (self.scrollView.width * len(self.scrollView.subviews), 0)
              		safe_bottom = self.bounds.max_y - self.objc_instance.safeAreaInsets().bottom
              		size = self.pageControl.sizeForNumberOfPages_(self.pageControl.numberOfPages())
              		self.pageControl.frame = CGRect(
              			(self.bounds.center().x - self.bounds.width / 2, safe_bottom - size.height), 
              			(self.bounds.width, size.height))
              
              		for v in self.scrollView.subviews:
              			v.width = self.bounds.width
              			v.height = self.bounds.height
              			v.x = v.pNum * self.bounds.width
              
              		self.set_page(self.pageControl.currentPage())
              
              	def scrollview_did_scroll(self, scrollView):
              		pageNumber = round(self.scrollView.content_offset[0] / (self.scrollView.content_size.width/len(self.scrollView.subviews)+1))
              		self.pageControl.currentPage = pageNumber
              		self._trigger_delegate()
              
              	def add_subview(self, v):
              		v.pNum = len(self.scrollView.subviews)
              		self.pageControl.numberOfPages = len(self.scrollView.subviews) + 1
              		self.scrollView.add_subview(v)
              		self.layout()
              
              	def _objc_color(self, color):
              		return UIColor.colorWithRed_green_blue_alpha_(*ui.parse_color(color))
              
              	def _py_color(self, objc_color):
              		return tuple(
              			[c.floatValue()
              				for c in objc_color.arrayFromRGBAComponents()]) if objc_color else None
              
              	def _trigger_delegate(self):
              		if hasattr(self, 'delegate'):
              			if hasattr(self.delegate, 'page_changed'):
              				if self.pageControl.currentPage() is not self._prev_page:
              					self.delegate.page_changed(self.pageControl.currentPage())
              					self._prev_page = self.pageControl.currentPage()
              
              	def set_page(self, page_number):
              		if page_number < self.pageControl.numberOfPages() and page_number > -1:
              			x = page_number * self.scrollView.width
              			self.scrollView.content_offset = (x, 0)
              		else:
              			raise ValueError("Invalid Page Number. page_number is zero indexing.")
              
              	@property
              	def page_count(self):
              		return self.pageControl.numberOfPages()
              
              	@property
              	def current_page(self):
              		return self.pageControl.currentPage()
              
              	@property
              	def hide_on_single_page(self):
              		return self.pageControl.hidesForSinglePage()
              	
              	@hide_on_single_page.setter
              	def hide_on_single_page(self, val):
              		self.pageControl.hidesForSinglePage = val
              
              	@property
              	def indicator_tint_color(self):
              		"""Returns un-selected tint color, returns None as default due to .pageIndicatorTintColor() returning that"""
              		return self._py_color(self.pageControl.pageIndicatorTintColor())
              
              	@indicator_tint_color.setter
              	def indicator_tint_color(self, val):
              		self.pageControl.pageIndicatorTintColor = self._objc_color(val)
              
              	@property
              	def indicator_current_color(self):
              		"""Returns selected tint color, returns None as default due to .currentPageIndicatorTintColor() returning that"""
              		return self._py_color(self.pageControl.currentPageIndicatorTintColor())
              
              	@indicator_current_color.setter
              	def indicator_current_color(self, val):
              		self.pageControl.currentPageIndicatorTintColor = self._objc_color(val)
              
              	def update(self):
              		print('!')
              		
              	def present(self, **kwargs): # evaluate input before presenting
              		if kwargs['hide_title_bar']:
              			self.scrollView.y=20 #if hide_title_bar then adjust scrollView
              		super(PageControl, self).present(**kwargs) # Continue..
              if __name__ == '__main__':
              	
              #	class SampleDelegate():
              #		def __init__(self):
              #			pass
              		
              #		def page_changed(self, page_number):
              #			"""Gets called every time the page changes."""
              #			print(f'Delegate: {page_number}')
              
              
              #	def btn_action(sender):
              #		m.set_page(2)
              			
              #	m = PageControl()
              
              #	sv = ui.View()
              #	sv.background_color = 'red'
              #	sv1B = ui.Button()
              #	sv1B.title = "Go To Page 3 (index 2)"
              #	sv1B.frame = (100,100,200,100)
              #	sv1B.action = btn_action
              #	sv.add_subview(sv1B)
              
              #	sv2 = ui.View()
              #	sv2.background_color = 'blue'
              
              #	sv3 = ui.View()
              #	sv3.background_color = 'yellow'
              
              #	m.indicator_tint_color = 'brown'
              #	m.indicator_current_color = 'black'
              
              #	m.add_subview(sv)
              #	m.add_subview(sv2)
              #	m.add_subview(sv3)
              	
              #	m.delegate = SampleDelegate()
              
              #	print(m.indicator_tint_color)
              #	print(m.indicator_current_color)
              #	print(m.page_count)
              #	print(m.current_page)
              #	print(m.hide_on_single_page)
              
              #	m.present('fullscreen
              
              	image_names = ['test:Boat', 'test:Lenna', 'test:Mandrill', 'test:Peppers']
              
              	pages = PageControl(
              		background_color='black',
              		indicator_tint_color='grey',
              		indicator_current_color='white')
              	
              	for image_name in image_names:
              		pages.add_subview(ui.ImageView(
              			image=ui.Image(image_name),
              			content_mode=ui.CONTENT_SCALE_ASPECT_FIT))
              			
              	pages.present(style='fullscreen', 
              		hide_title_bar=False
              	)
              	
              

              as you can see, just by lowering the scrollView 20pt (i believe tjis is the title bar height), we fix the issue. by placing an override in our PageControl class we check if hide_title_bar and only if True we then lower the y attribute.

              I tested and even after changing y the childview's image can still completely cover the screen. this tells myself that somthing behind the scenes is not right. the image should not be able to display above 20 since the parent view is set to 20 for y.

              @omz i also noticed that there is an issue if you stack too deep (3 deep in my case) in the UI Builder. App doesnt seem to hold onto the objects data and they disapear when you leave subviews for the given custom View object.

              Main View
              - sub View
              - - sub sub View
              - - - datalost...
              

              maybe in this case we might be feling a bit of this with the ImageView we are using?

              Obj-c instance
              - PageControl
              - - ScollView
              - - - ImageView
              

              EDIT: what i mean is the imageview might be losing its placement data from creation so it defaults to having a tittle bar, and then when we initiate the event loop we remind it of its placement when delegate for scrolling gets fired..

              And ofcourse this is with me asuming two things..

              1. objc instance and python are separate intities
              2. if so then objc takes persistance over the python view.

              hopfully this helps get an idea of whats going on and hopfully im not wrong on these. this is just my perspective and im by no means an expert.

              mikael 1 Reply Last reply Reply Quote 0
              • mikael
                mikael @stephen last edited by

                @stephen, probably related to the 20 pt at the top, yes. I would really like to resolve it in a more generic way than putting in a specific check for the hide_title_bar.

                Couple of data points regarding your other observations:

                • ImageViews, by design or accident, do not have their clipsToBounds set. This is why the images do not seem to respect the 20 pt boundary.
                • I have built many UIs with nesting deeper than 3 layers, and would say probably that is not the cause here.

                I suspect there is something around the handling of the safe area that has changed around iOS 11 and has not been really addressed by Pythonista since.

                What do you get on iPad if you check the safeAreaInsets for a fullscreen view?

                mikael 1 Reply Last reply Reply Quote 0
                • mikael
                  mikael @mikael last edited by

                  @stephen, yes, this seems to be a bug or a feature, but not in @Samer’s component.

                  As a workaround, we can include an additional background view:

                  background = ui.View(background_color='black')
                  

                  And then set the PageControl size only after the background has been presented:

                  background.present('fullscreen', 
                      hide_title_bar=True
                  )
                  
                  pages.frame = background.bounds
                  background.add_subview(pages)
                  

                  No jumping around then.

                  1 Reply Last reply Reply Quote 2
                  • mikael
                    mikael @Samer last edited by

                    @Samer, couple more simplification/maintainability comments, which do not affect functionality.

                    Forgiveness, not permission

                    For the delegate calling, the pythonic way is to rely a bit more on the exception mechanism:

                    try:
                        callback = self.delegate.page_changed
                    except AttributeError: return
                    if self.pageControl.currentPage() is not self._prev_page:
                        callback(self, self.pageControl.currentPage())
                        self._prev_page = self.pageControl.currentPage()
                    

                    This is not in any way inefficient (hasattr relies on catching the same exception), is much more focused on the "good case", and avoids a fair bit of nesting.

                    Oh, I lied, there is a functionality change there: recommend adding self as the first parameter to the callback. This enables using two or more instances of the component, all tracked with the same delegate, which then gets the information which component was triggered. This is in line how Button actions have a sender, TextView delegates get the textview etc.

                    Minimize attributes

                    In add_subview, we add a pNum attribute to the pages. This is unnecessary, as we can change the layout method to simply use the order of subviews:

                    for i, v in enumerate(self.scrollView.subviews):
                        v.x = i * self.bounds.width
                    

                    This even supports the future option of reorganising the views, without needing to fix the pNum.

                    Let iOS do the resizing wherever possible

                    If in add_subview we set the page size and flex:

                    def add_subview(self, page):
                        page.frame = self.scrollView.bounds
                        page.flex = 'WH'
                        ...
                    

                    ... we do not need to set v.width and v.height explicitly in the layout loop, so it reduces to the simple version above. I imagine native iOS resizing is slightly more efficient as well.

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

                      @mikael

                      sorry for the delay brotha heres a rundown per call of layout without hardsetting the y to 20

                      ⇨⇨ Results

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

                        I took a few days to think about this bug/feature. I implemented a workaround for it which at the very least hides the problem. I don’t like it, but it works. If anyone has a better solution then overriding the present method, I would be happy to replace my current solution with it.

                        stephen 1 Reply Last reply Reply Quote 0
                        • stephen
                          stephen @Samer last edited by stephen

                          @Samer

                          im sorry i never posted.. you dont need to override present. in your __init__ just add y=20 to your scrollView creation.

                          
                          
                          class PageControl(ui.View):
                              def __init__(self, **kwargs):
                                  
                                  
                                  self.scrollView = ui.ScrollView(
                                      delegate=self,
                                      paging_enabled=True,
                                      shows_horizontal_scroll_indicator=False,
                                      bounces=False,
                                      frame=self.bounds,
                                      flex='WH',
                                      y=20 # <-- here
                                  )
                          
                          

                          just one line , 4 characters 🤓

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

                            i only overrided present cuz i tent to make stuff complicated when im tired lol i apologize

                            Samer 1 Reply Last reply Reply Quote 0
                            • Samer
                              Samer @stephen last edited by

                              @stephen There are two problems I encountered with that solution, the first is that when hide_title_bar=True The whole view is shifted down 20px. The second is that on my iPad it still jumps down a few pixels, yet it doesn’t on my iPhone. I wish the solution was that simple however it is unfortunately not.

                              stephen 1 Reply Last reply Reply Quote 0
                              • stephen
                                stephen @Samer last edited by stephen

                                @Samer Sorry for the late reply. been working on this one all night.

                                my post

                                I still dont have the ideal fix for this.. (meaning without overriding present) but am u got three choices..

                                ONE:

                                Hard setting scrollView() like before..

                                
                                
                                self.scrollView = ui.ScrollView(
                                		delegate=self,
                                		paging_enabled=True,
                                		shows_horizontal_scroll_indicator=False,
                                		bounces=False,
                                		frame=self.bounds,
                                		flex='WH',
                                		y=20 # ⥢⬅ here
                                	)
                                	
                                

                                TWO:

                                Hard set inside SetPage..

                                
                                def set_page(self, page_number):
                                		if page_number < self.pageControl.numberOfPages() and page_number > -1:
                                			x = page_number * self.scrollView.width
                                			self.scrollView.content_offset = (x, -20) # ⥢⬅ here 
                                		else:
                                			raise ValueError("Invalid Page Number. page_number is zero indexing.")
                                			
                                
                                

                                THREE:

                                Your favorit override..
                                added an instance variable to it though..

                                
                                class PageControl(ui.View):
                                	def __init__(self, **kwargs):
                                	
                                		self.titleBarAdjustment=0 # ⥢⬅ here 
                                		
                                		...
                                
                                	#with this guy..
                                	def present(self, **kwargs): # evaluate input before presenting
                                			if kwargs['hide_title_bar']:
                                				self.scrollView.y=20 #if hide_title_bar then adjust scrollView
                                			super(PageControl, self).present(**kwargs) # Continue..
                                
                                

                                the second option needs to know if the bar is present otherwise it just has a revers effect to original image issue.. (jumps up)

                                im still looking for a beter solution for this. the objc methods were not working last night and im sure it was writers error. but im the mean time at least two of these will hold by maybe?


                                as for your other issues..

                                Depending on your phone and tabelets they could be explained.

                                i suspect that it has todo with Retina scaling and resolution. i have an iPhone 6s and iPad Air 2 and they both have a 2x1 Retina Display so i dont get this issue but id imagine if u has a 2x1 phone and 3x1 ipad it would creat this. all you really need to do if thats the case is check device at runtime and setup Layout accordingly. ui/scene, atleast for 2x1, should auto compensate for this but pil doesnt. just for future info lol.. but by using ui.from_data(data, scale) is where u set scaling for retina convertion. this could help if your dispay happends to be a factor. if i find anything ill let you know!

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