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)
-
@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. -
After a few week break, I have just updated this post with a new version that implements some of the great feedback I got.
-
@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 byobjc_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 thechangePage
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 withsuper().add_subview()
. -
Sizing of the pages would be best based on
self.bounds.width
andself.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 internalbounds
instead of the externalframe
, just to tolerate the remote possibility of someone adding atransform
to your component.) -
Strongly recommend moving all the placement code to a
layout
method, where you just iterate through the scrollViewsubviews()
, size them according toself.bounds
, place them side-by-side and updatecontent_size
.layout
gets called on rotation if the component is set toflex
, andadd_subview
can call it directly. This makes the component completely responsive, and the developer can justadd_subview
s 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 oftintColor
. 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, changechangePageClass
to start with an upper case letter to signal that it is a class.
-
-
- 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')
-
@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.
-
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
Moduledef did_change_size(self): pass
-
@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
-
-
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 witharrayFromRGBAComponents()
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!
-
I have just edited the post with a new version. (v0.3 as i’m calling it).
-
@Samer, thanks.
Couple of bug fixes:
- After moving
changePage
out of the init, it no longer works, becausetarget
goes out of scope. This is easily fixed by retaining the reference, i.e. usingself.target
everywhere where you hadtarget
, or maybeself._target
since it is not exactly part of your component's "public API". - As a property,
tint_color
conflicts withui.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 suggestindicator_tint_color
andindicator_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, inadd_subview
and is thus harmless, but if developer set the view'sx
to some value before adding the subview, would cause problems. Recommend removing if you did not have some specific idea for it. -
ui.View
s have a handy feature where they apply all keyword arguments toself
. Combined with having properties this means that we do not need to explicitly include those in our init parameters, or have theif
sections of code, as long as we add**kwargs
to the init parameters and callsuper().__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, thesuper()...
call needs to be at the end of the init to haveself.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
andflex
above – this removes the need to manually keep the ScrollView's size in sync with your component in thelayout
method. (Also used so often that I have snippet for theframe
/flex
combo.)
And feature suggestions:
- Exposing a
page_changed
callback with adelegate
parameter forself
. - 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.
- After moving
-
@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))
-
size.width / 2 --> size.width // 2 to deal with size.width being odd?
-
@ccc, screen coordinates are floats.
-
@mikael Thank you again for this feedback.
What do you mean by:
- Exposing a
page_changed
callback with adelegate
parameter forself
.
I have not played around with callbacks very much and would appreciate some guidance on how to implement something like this.
- Exposing a
-
@Samer, I guess it sounds grander than it is.
What I meant is that in the
scrollview_did_scroll
method, you could check ifself
has an attributedelegate
, and if that object has a callable attribute calledpage_changed
, then we call that with some suitable parameters. Following the other Pythonista UI classes, parameters could beself
andpage_number
.scrollview_did_scroll
gets called a lot, not just once per page, so we would need to keep track of the previous page number and only call the delegate when there is a change.End result, people can set a delegate on your component to get notified when the page changes.
-
@mikael are you talking about somthing like this?
class MyDel: def __init__(self, sv): self.sv=sv self.current_page=sv.current_page self.prev_page=None self._refresh() def page_changed(self, scroll_view, page): pass def _refresh(self): if self.sv: if self.current_page != self.sv.current_page: self.page_changed(self.sv, self.sv.current_page) self.prev_page=self.current_page self.current_page=self.sv.current_page self.sv.refresh() class MyScroll(ui.View): def __init__(self): self.delegate=MyDel(self) self.current_Page=0 def _refresh(self): if self.delegate: self.delegate.refresh()
-
@stephen, not quite. The delegate (or the developer using the page control component) should not need to think about whether the page has changed; it only gets a notification when the page has changed.
Thus no
_refresh
, no book-keeping in the user's side, just aself.previous_page
to compare to in thescrollview_did_scroll
method of @Samer's component. -
@mikael ok i didnt click we are using an already exsisting callback method. i was thinking of a Base Class form implemention.
so like this?
class MyDel: def __init__(self, sv): self.current_page=sv.current_page self.prev_page=None def page_changed(self, sv, new-page): pass def scrollview_did_scroll(self, scrollview): if self.sv == scrollview: if self.current_page != scrollview.current_page: self.page_changed(scrollview, scrollview.current_page) self.prev_page=self.current_page self.current_page=self.sv.current_page self.sv.refresh() class MyScroll(ui.View): def __init__(self): self.delegate=MyDel(self) self.current_Page=0
but what happends if
self.current_page
changes with no call to delegatescrollview_did_scroll
due to button activation or key entry? wouldnt that delay any needed action on a method, for example, calledpage_opend
orpage_should_open
?i dont mean to over think i just want to make sure i understand completely 😅
EDIT:
wnted to clerify i do understand you mean use self as delegate and not other object i just separated for clearity. 😊
-
I just edited the post with v0.4. Let me know what you people think!