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)
-
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!
-
@Samer Good work brother 😊 is this for exploration of
ui
module or project? -
@stephen A bit of both, I have a project in the works that started out as a way to explore the ui module, it then morphed into learning about the objc_utils module a bit (MapView, ProgressView, UIPageControl) and then finally into wanting to create an app for the app store. This is just one part of a larger project. However I have probably had the most fun on this one part then all the others combined.
-
@Samer, good job!
Just a few more things come to mind. Seems like this fun is coming to an end.
- Recommend adding an explicit call to
self.layout()
at the end ofadd_subview
. Layout does get called without it, but only later, triggered by the UI loop, which can cause flickering when a view is momentarily visible in the wrong place. _trigger_delegate
is called both inset_page
andscrollview_did_scroll
. Sinceset_page
sets thecontent_offset
which then triggersscrollview_did_scroll
, you should not need to call_trigger_delegate
inset_page
at all.- Also, it would make more sense to me to call
_trigger_delegate
last inscrollview_did_scroll
, after the new page number has been set. The way it is now works only, I think, because there is so many scrolling events in every move from page to page. - Recommend moving
sampleDelegate
(should be upper case) andexample
within theif __name__
block. That way they do not unnecessarily get imported in "prudction use", and pollute the component's namespace.
- Recommend adding an explicit call to
-
@mikael And with that all the changes are implemented.
-
@Samer, tried it out, just to get the ”developer experience”.
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('fullscreen', hide_title_bar=True )
Nice and tight, with nothing extra.
But, noticed a problem where on the first scroll the images jump down a little bit. I also tried changing the content size to the actual vertical content size, but then ended up having vertical scrolling, and images below the vertical center.
@Samer, which device are you using, are you seeing the same?
-
This post is deleted! -
This post is deleted! -
For some reason
self.scrollView.y
is 20pt too high and it fails to flexH
i setframe
andflex
and issue was corrected. settingy
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',)
-
@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.