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
    22494
    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.
    • Samer
      Samer last edited by Samer

      Hello!

      For a project i’m currently working on I needed a UIPageControl view, I thought I would share this here in case anyone else needed it. I apologize for my terrible code style. I am very open to criticism on anything I should change.

      
      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 present(self, *args, **kwargs):
      		if 'hide_title_bar' in kwargs and kwargs['hide_title_bar']:
      				#Temp work around for possible bug.
      				background = ui.View(background_color=self.background_color)
      				background.present(*args, **kwargs)
      				self.frame = background.bounds
      				background.add_subview(self)
      		else:
      			super().present(*args, **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 i, v in enumerate(self.scrollView.subviews):
      			v.x = i * 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, page):
      		self.pageControl.numberOfPages = len(self.scrollView.subviews) + 1
      		page.frame = self.scrollView.bounds
      		page.flex = 'WH'
      		self.scrollView.add_subview(page)
      		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):
      		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()
      
      	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)
      
      
      if __name__ == '__main__':
      	
      	class SampleDelegate():
      		def __init__(self):
      			pass
      		
      		def page_changed(self, sender, page_number):
      			"""Gets called every time the page changes."""
      			print(f'Sender: {sender}, Delegate: {page_number}')
      
      
      			
      	pages = 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)
      	
      	def btn_action(sender):
      		pages.set_page(2)
      	sv1B.action = btn_action
      	
      	sv.add_subview(sv1B)
      
      	sv2 = ui.View()
      	sv2.background_color = 'blue'
      
      	sv3 = ui.View()
      	sv3.background_color = 'yellow'
      
      	pages.indicator_tint_color = 'brown'
      	pages.indicator_current_color = 'black'
      
      	pages.add_subview(sv)
      	pages.add_subview(sv2)
      	pages.add_subview(sv3)
      	
      	pages.delegate = SampleDelegate()
      
      	print(pages.indicator_tint_color)
      	print(pages.indicator_current_color)
      	print(pages.page_count)
      	print(pages.current_page)
      	print(pages.hide_on_single_page)
      
      	pages.present('fullscreen')
      

      Gist: https://gist.github.com/samerbam/8062c6075283a2c190812ddc925c015a

      Changelog:

      v0.2:

      • m.scrollView.add_subview(view) -> m.add_subview(view)
      • m.tintColor to set un-selected dot colour
      • m.currentTintColor to set selected dot colour
      • No longer need to manually set x position on each page
        — x and y pos acts as offset from edge of screen (like normal)
      • Moved some objc class creations out of __init__ (still working on this)
      • Mirrored to gist

      v0.3:

      • Finished moving objc class creations out of __init__
      • m.TintColor -> m.tint_color
      • m.currentTintColor -> m.current_tint_color
      • m.current_page returns the current page your on
      • m.page_count returns total amount of pages
      • Switched from ui.get_screen_size() to using self.bounds
        — Should allow for compatibility with more advanced UI’s

      v0.4

      • Fixed bug where tapping on either side of dots to change page would only work on first launch.
      • m.tint_color -> m.indicator_tint_color
      • m.current_tint_color -> m.indicator_current_color
      • Added delegate callback page_changed set delegate to class that includes page_changed(self, page_number)
      • Added set_page method to change the page programmatically
      • Added hide_on_single_page attribute (defaults to true) hides dots with only one page.

      v0.5

      • Changed example slightly to include a weirdly placed button.
      • Added explicit call to self.layout() in add_subview
      • Removed call to _trigger_delegate from set_page, as set_page triggers scrollview_did_scroll which calls _trigger_delegate

      v0.6

      • Switched from hasattr to try: except AttributeError:
      • Implemented a work around for a bug involving the view jumping down ~20 pixels. (Really dislike this, however it seems like the best I could do at the moment)
      • Removed unnecessary attribute pNum
      • Switched from setting width and height of each subview manually to using flex
      • Renamed PageControl instance in example to pages from m
      mikael cvp 2 Replies Last reply Reply Quote 1
      • mikael
        mikael @Samer last edited by mikael

        @Samer, nice! I had not even realized there was a specific control for this. (And nothing wrong with your coding style.)

        Couple of further development suggestions, if you want to increase the overall reusability of the implementation.

        1. Move ObjC class creations outside init, to avoid creating them for every instance.
        2. Support adding views without explicitly setting a size or number of pages. Would suggest overriding add_subview for ultimate convenience.
        3. Tweak the previous to support device rotation and arbitrary sized parent ScrollView (set flex on PageControl, override layout).
        1 Reply Last reply Reply Quote 2
        • cvp
          cvp @Samer last edited by

          @Samer As you import UIColor, I suppose you know that you can define indicators colors 😀

                  self.pageControl.setPageIndicatorTintColor_(ObjCClass('UIColor').color( red=0, green=0,  blue=0, alpha=1.0))	
                  self.pageControl.setCurrentPageIndicatorTintColor_(ObjCClass('UIColor').color( red=1, green=1,  blue=1, alpha=1.0))	 
          

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

            @cvp, yes, add the colors as constructor parameters for PageControl, but by default follow the theme colors.

            When setting ObjC colors, I like to support all variations of defining the colors by using ui.parse_color:

            UIColor.colorWithRed_green_blue_alpha_(*ui.parse_color('teal'))
            
            cvp 1 Reply Last reply Reply Quote 1
            • cvp
              cvp @mikael last edited by

              @mikael Understood, but you have to know the color name in English, don't you?
              I prefer set as an r,g,b, but all is allowed, no best way

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

                @cvp, my point was that if you want to create a reusable component with color parameters, the construct works just as well with any way the developer wants to define the color:

                import objc_util
                import ui
                
                UIColor = objc_util.ObjCClass('UIColor')
                
                def objc_color(color):
                    return UIColor.colorWithRed_green_blue_alpha_(*ui.parse_color(color))
                
                print(objc_color('red'))
                print(objc_color((1,0,0)))
                print(objc_color((1,0,0,1)))
                print(objc_color('#F00'))
                
                cvp 1 Reply Last reply Reply Quote 2
                • cvp
                  cvp @mikael last edited by

                  @mikael ok, finally, I did not understand correctly 😢, sorry

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

                    @mikael Thanks for the feedback! I have a few questions about how I would go about overriding add_subview. How would I add the subview to the view.

                    This snippet:

                    def add_subview(self, v):
                    	self.scrollView.add_subview(v)
                    

                    Gives me the error:
                    ValueError: Cannot add self as subview.

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

                      @Samer, the method looks good to me. How are you calling it?

                      In my mind, the point of this method would be that it takes care of placing the new view to the right of the previous view, as well as updating the content_size and page counts, i.e. these move away from the __init__ and the user of the component does not need to worry about the placements of the views.

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

                        After a few week break, I have just updated this post with a new version that implements some of the great feedback I got.

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

                          @Samer, thanks for bearing with my comments. I enjoy this kind of almost pair programming, but just let me know if you get fed up with my nitpicking.

                          Some further suggestions:

                          • UIColor is already defined by objc_util. (Not documented, but code completion shows it.)

                          • You can move the ObjC class creation out of init if, instead of using closure, you set target.page_control = self in init, and access the values in the changePage method with:

                            self = ObjCInstance(_self)
                            self.page_control
                            self.page_control.scrollView
                            

                            (This works with attributes but not as well with methods, due to the way ObjCInstance proxy is implemented.)

                          • The color properties are convenient, but return None if the defaults are not changed. I would prefer just to use a "reverse" color function, see below. It would have the additional benefit of removing the extra internal variables.

                            def _py_color(self, objc_color):
                                return tuple([c.floatValue() for c in objc_color.arrayFromRGBAComponents()])
                            
                          • You can just name the method add_subview. ScrollView can be added with super().add_subview().

                          • Sizing of the pages would be best based on self.bounds.width and self.bounds.height, instead of the screen size, which is a very specific case and prevents anyone using the component as a part of a more complicated UI. (And the internal bounds instead of the external frame, just to tolerate the remote possibility of someone adding a transform to your component.)

                          • Strongly recommend moving all the placement code to a layout method, where you just iterate through the scrollView subviews(), size them according to self.bounds, place them side-by-side and update content_size. layout gets called on rotation if the component is set to flex, and add_subview can call it directly. This makes the component completely responsive, and the developer can just add_subviews without worrying about placement.

                          • (One more read-only convenience property comes to mind, page_count, not exactly sure what for, though.)

                          • Last but not least, I would change the naming of all externally visible methods and properties to be Pythonic, i.e. tint_color instead of tintColor. While only syntactic, it is in line with all Pythonista modules, and tells the developer that they do not have to understand any of the ObjC parts of using your component. And while we are at it, change changePageClass to start with an upper case letter to signal that it is a class.

                          Samer 1 Reply Last reply Reply Quote 2
                          • stephen
                            stephen last edited by stephen

                            • Hope i didnt Scratch it up too bad.. 😬😅😬 *
                            from ui import (ScrollView, View, get_screen_size, Label, ALIGN_CENTER)
                            from objc_util import (ObjCClass, CGRect, create_objc_class, ObjCInstance)
                            from random import random as rnd
                            
                            
                            class PageControl(View):
                                def __init__(self, tintColor=None, currentTintColor=None):
                                    self.w, self.h = get_screen_size()
                                    
                                    self.pages=dict({})
                            
                                    self.scrollView = ScrollView(frame=(0,0,self.w, self.h), delegate=self, paging_enabled=True, bounces=False, shows_horizontal_scroll_indicator=False )
                                    
                                    self.add_subview(self.scrollView)
                                    
                                    self.pageControl = ObjCClass('UIPageControl').alloc().initWithFrame_(CGRect((0,self.h*0.8),(100,100))).autorelease()
                                    
                                    self.pageControl.addTarget_action_forControlEvents_(create_objc_class("target", methods=[self.changePage]).new().autorelease(), 'changePage', 1<<12) #1<<12 = 4096
                                    
                                    self.pageControl.numberOfPages = len(self.scrollView.subviews)
                                    
                                    self_objc = ObjCInstance(self).addSubview_(self.pageControl)
                                    
                                    if tintColor:
                                        self.pageControl.pageIndicatorTintColor = self._objc_color(tintColor)
                                        
                                    if currentTintColor:
                                        self.pageControl.currentPageIndicatorTintColor = self._objc_color(currentTintColor)
                            
                                def scrollview_did_scroll(self, scrollView):
                                    self.pageControl.currentPage = round(
                                            scrollView.content_offset[0] / scrollView.width)
                            
                                def _add_subview(self, v):
                                    v.x = v.x + len(self.scrollView.subviews)*self.scrollView.width
                                    self.scrollView.add_subview(v)
                                    self.pageControl.numberOfPages = len(self.scrollView.subviews) 
                                    self.scrollView.content_size = (self.scrollView.width * (len(self.scrollView.subviews)), 0)
                                    
                            
                                def changePage(self, cmd):
                                    self.scrollView.content_offset = (self.pageControl.currentPage() * self.scrollView.width, 0)
                            
                                def _objc_color(self, color):
                                    return ObjCClass('UIColor').colorWithRed_green_blue_alpha_(*parse_color(color))
                            
                                @property
                                def tintColor(self):
                                    return self._tintColor
                            
                                @tintColor.setter
                                def tintColor(self, val):
                                    self.pageControl.pageIndicatorTintColor = self._objc_color(val)
                            
                                @property
                                def currentTintColor(self):
                                    return self._currentTintColor
                            
                                @currentTintColor.setter
                                def currentTintColor(self, val):
                                    self.pageControl.currentPageIndicatorTintColor = self._objc_color(val)
                                
                                def PageTitle(self, tag):
                                    l= Label(text=tag, font=('<System-Bold>', 16))
                                    l.text_color=(0.25, 0.14, 0.01, 1.0)
                                    l.width = 250
                                    l.alignment=ALIGN_CENTER
                                    l.x, l.y= self.w/2-l.width/2, l.height+20
                                    return l
                                
                                def addPage(self, tag="", frame=None, bgc=None, **kwargs):
                                    if not bgc:
                                        bgc = r, g, b, a=rnd()*0.5+0.35, rnd()*0.5+0.35, rnd()*0.5+0.35, 1.0
                                    if not frame:
                                        frame =(0, 0, self.w, self.h)
                                    v= View(name=tag, frame=frame, background_color=bgc, **kwargs)
                                    if tag:
                                        self.pages[tag]=v
                                        v.add_subview(self.PageTitle(tag))
                                    self._add_subview(v)
                                    return v
                                    
                            if __name__ == '__main__':
                                m = PageControl()
                                
                                m.addPage('Table of Content', bgc="white")
                                
                                m.addPage('Index')
                                
                                for chap in range(1, 11):
                                    m.addPage(f"chapter {chap}")
                            
                                m.present('fullscreen')
                            
                            1 Reply Last reply Reply Quote 1
                            • Samer
                              Samer @mikael last edited by

                              @mikael I really enjoy this type of programming as well, It gives me an external view and makes me aware of certain use cases I was not aware of.

                              • Thanks for letting me know of super().add_subview() wish I had known about that earlier :|
                              • Could you explain what you mean by a layout method? I’m relatively new to pythonista (not so much python itself) and the UI module.
                              mikael 1 Reply Last reply Reply Quote 0
                              • stephen
                                stephen last edited by stephen

                                @Samer

                                Layout

                                pythonista Docs

                                
                                def layout(self):
                                        # This will be called when a view is resized. You should typically set the
                                        # frames of the view's subviews here, if your layout requirements cannot
                                        # be fulfilled with the standard auto-resizing (flex) attribute.
                                        pass
                                
                                

                                often used to signal when screen orientation has changed.

                                also in scene Module

                                def did_change_size(self):
                                	pass
                                
                                1 Reply Last reply Reply Quote 2
                                • stephen
                                  stephen last edited by stephen

                                  @Samer Heres a scene example being used to adust the gui when orientation changes. i apologize about the clutter i just grabbed from a current project so the codevwont run as is. just a visual use example. hope it helps..

                                  
                                  class UICanvas(GameObject):
                                  	def __init__(ns, size,  *args, **kwargs):
                                  		GameObject.__init__(ns)
                                  		ns.size=size
                                  		ns.OverlaySize=Point(0.0, 0.0)
                                  		ns.joySize=Point(128, 128)
                                  		
                                  	def awake(ns):
                                  		pass
                                  		
                                  	def start(ns):
                                  		w, h = ns.size
                                  		
                                  
                                  	def OnEvent(ns, e):
                                  		return
                                  		
                                  	def fixed_update(ns):
                                  		pass
                                  		
                                  	def JoyStick(ns, w, h):
                                  		zp=0
                                  		xs=1.0
                                  		ys=1.0
                                  		a=1.0
                                  		spd=1.0
                                  		par=ns
                                  		sz=Size(64, 64) if w > 700 else Size(40, 40)
                                  		pos=Point(0,0)
                                  		
                                  		if  w > h:
                                  			if w > 700:
                                  				pos=Point(w-w/8, h/6)
                                  			else:
                                  				pos=Point(w-w/8, h/6)
                                  		elif w < h:
                                  			if w > 700:
                                  				pos=Point(w-w/6, h/8) 
                                  			else:
                                  				pos=Point(w-w/6, h/10) 
                                  		
                                  		
                                  		col=(0.91, 0.99, 1.0, 1.0)
                                  		bm=0
                                  		ap=Point(0.5, 0.5)
                                  		TJoy=cache['overlay-joy']
                                  		if 'joy' in UIObjects.keys() and UIObjects['overlay'].size != size:
                                  			UIObjects['joy'].remove_from_parent()
                                  			UIObjects.pop('joy', None)
                                  		elif 'joy' not in UIObjects.keys():
                                  			UIObjects['joy']=None
                                  		else:
                                  			return True
                                  		UIObjects['joy']=UIComponent(TJoy, position=pos, z_position=zp,
                                  					alpha=a, speed=spd, parent=par, size=sz, color=col,
                                  					blend_mode=bm, anchor_point=ap)
                                  	
                                  	def Overlay(ns, w, h):
                                  		pos=Point(0.0, 0.0)
                                  		zp=0
                                  		xs=1.0
                                  		ys=1.0
                                  		a=1.0
                                  		spd=1.0
                                  		par=ns
                                  		sz=Size(w, h)
                                  		col=(0.91, 0.99, 1.0, 1.0)
                                  		bm=0
                                  		ap=Point(0.0, 0.0)
                                  
                                  		TLand=cache['overlay-land']
                                  		TPort=cache['overlay-port']
                                  		
                                  		if 'overlay' in UIObjects.keys() and UIObjects['overlay'].size != size:
                                  			UIObjects['overlay'].remove_from_parent()
                                  			UIObjects.pop('overlay', None)
                                  		elif 'overlay' not in UIObjects.keys():
                                  			UIObjects['overlay']=None
                                  		else:
                                  			return True
                                  		if w < h:
                                  			if w<700:
                                  		 		sz=Size(w, h/10*8)
                                  			UIObjects['overlay']=UIComponent(TPort, position=pos, z_position=zp,
                                  					alpha=a, speed=spd, parent=par, size=sz, color=col,
                                  					blend_mode=bm, anchor_point=ap)
                                  		elif w > h:
                                  			UIObjects['overlay']=UIComponent(TLand, position=pos, z_position=zp,
                                  					alpha=a, speed=spd, parent=par, size=sz, color=col, 
                                  					blend_mode=bm, anchor_point=ap)
                                  	
                                  	def AddUIObject(ns, k, v):
                                  		ns.add_child(v)
                                  		UIObjects[k]=v
                                  		ns.tally.components+=1
                                  
                                  	def refresh(ns, ss):
                                  		w, h=ss
                                  		ns.Overlay(w, h)
                                  		ns.JoyStick(w, h)
                                  	
                                  
                                  
                                  class Loop(Scene):
                                  	__name__='GameLoop'
                                  	def setup(ns):
                                  		ns.background_color=(0.5, 0.5, 0.5)
                                  		PopulateCache()
                                  		for k,v in cache.items():
                                  			cache[k]=Texture(v)
                                  			
                                  		ns.canvas=UICanvas(ns)
                                  		ns.canvas.refresh(ns.size)
                                  		ns.add_child(ns.canvas)
                                  		
                                  		ns.actor1=Actor(cache['hitbox-solid'], 'test, Actor1', size=ns.size/10,
                                  						 position=ns.size/2, parent=ns)
                                  		ns.tally=Tally(['gameobjects', 'giobjects', 'resources', 'mobs', 'npcs'])
                                  		
                                  	def update(ns):
                                  		pass
                                  	
                                  	def did_change_size(ns):
                                  		SCREEN_RESOLUTION=ns.size
                                  		ns.canvas.refresh(ns.size)
                                  	
                                  	def AddObject(ns, k, v):
                                  		pass
                                  
                                  	def LoadWorld(ns):
                                  		pass
                                  
                                  
                                  
                                  
                                  1 Reply Last reply Reply Quote 1
                                  • mikael
                                    mikael @Samer last edited by

                                    @Samer, what @stephen said. It is a method you override in your ui.View subclass to react to changes in the view's size, usually due to rotation.

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

                                      I have a quick question about:

                                      • The color properties are convenient, but return None if the defaults are not changed. I would prefer just to use a "reverse" color function, see below. It would have the additional benefit of removing the extra internal variables.
                                      def _py_color(self, objc_color):
                                         return tuple([c.floatValue() for c in objc_color.arrayFromRGBAComponents()])
                                      

                                      Specifically what you were referring to with arrayFromRGBAComponents() I wasn’t able to find a reference to it anywhere. Also i’m assuming that by objc_color I would be passing in the UIColor that _objc_color creates.
                                      Scratch that, Turns out I was passing in the wrong thing.

                                      Thank you everyone for all the help, I really appreciate it!

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

                                        I have just edited the post with a new version. (v0.3 as i’m calling it).

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

                                          @Samer, thanks.

                                          Couple of bug fixes:

                                          • After moving changePage out of the init, it no longer works, because target goes out of scope. This is easily fixed by retaining the reference, i.e. using self.target everywhere where you had target, or maybe self._target since it is not exactly part of your component's "public API".
                                          • As a property, tint_color conflicts with ui.View's property by the same name, so we need to give it another name. To make them easier to remember and consistent, I would suggest indicator_tint_color and indicator_current_color – a bit verbose, but maybe we prioritize clarity over brevity as they are unlikely to be typed a lot.
                                          • On phones the control moves just outside the screen when the phone is rotated – and even if we change the multiplier to a smaller value, there is a chance that it hits the "swipe up from the bottom" indicator that the phones have. To make this more robust, I suggest we let iOS handle it by:
                                            • Using the safe area to avoid any on-screen extras.
                                            • Letting the control calculate the right size for it.
                                              ... but especially the safe area is a bit tricky, let me get back to you when I have more time.

                                          Simplification suggestions:

                                          • contentOffset looks to me like maybe outdated code, as it is set to 0, in add_subview and is thus harmless, but if developer set the view's x to some value before adding the subview, would cause problems. Recommend removing if you did not have some specific idea for it.

                                          • ui.Views have a handy feature where they apply all keyword arguments to self. Combined with having properties this means that we do not need to explicitly include those in our init parameters, or have the ifsections of code, as long as we add **kwargs to the init parameters and call super().__init__(**kwargs) somewhere in the init code. (I use this so often that I have made it into a Pythonista snippet.) In the case of your component, the super()... call needs to be at the end of the init to have self.pageControl created and available.

                                          • Not a necessary change for this code, but an idea for future projects: the same keyword-setting idea works with other views, so you could avoid typing self.scrollView n times and make the ScrollView creation more readable by changing it to:

                                              self.scrollView = ui.ScrollView(
                                                  delegate=self,
                                                  paging_enabled=True,
                                                  shows_horizontal_scroll_indicator=False,
                                                  bounces=False,
                                                  frame=self.bounds, flex='WH',
                                              )
                                            
                                          • Also, note the suggestion on how to set the frame and flex above – this removes the need to manually keep the ScrollView's size in sync with your component in the layout method. (Also used so often that I have snippet for the frame/flex combo.)

                                          And feature suggestions:

                                          • Exposing a page_changed callback with a delegate parameter for self.
                                          • Including a set_page method to scroll to a specific page programmatically. To keep things neat, changePage could also call the same method.
                                          • Adding a property to hide the control when there is only one page (hidesForSinglePage), and going against Apple, I would say the default should be True.
                                          Samer 1 Reply Last reply Reply Quote 1
                                          • mikael
                                            mikael @Samer last edited by

                                            @Samer, ok, here’s how to set the control size and position safely on all devices:

                                            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 - size.width/2,
                                                safe_bottom - size.height),
                                                (size.width, size.height))
                                            
                                            1 Reply Last reply Reply Quote 1
                                            • First post
                                              Last post
                                            Powered by NodeBB Forums | Contributors