UIScrollView - how is showsHorizontalScrollIndicator calculated? - ios

If showsHorizontalScrollIndicator is enabled, a small horizontal indicator is displayed at the bottom of the UIScrollView. I was wondering if anyone could tell me how this is calculated. The reason being, I want to display a custom indicator in a horizontal navigation system if there is content left/right, show/hiding when content is available.

I just use scrollView.contentOffset.x. If you want to know what page you are on, you divide by your page width (assuming you have a standard width). So
int currentPage=(int)scrollView.contentOffset.x/SCREEN_WIDTH; for example. Now if you want to display a position indicator, like in the iOS homepage, you can do it in a loop in
-(void)scrollViewDidScroll:(UIScrollView *)sender something like
currentPage=(int)scrollView.contentOffset.x/SCREEN_WIDTH;
for(int i=0; i<pageCount; i++){
if(i==currentPage)//display a filled in 'o'
else //display an empty 'o'
}
Oh, BTW, -(void)scrollViewDidScroll:(UIScrollView *)sender is not called when the UIScrollView is initially loaded, so you perhaps should call it yourself (in order to display the indicator before the user has scrolled). You can also use scrollView.contentOffset.y off course, so you could easily adapt this to show a '+' shaped arrangement of 'o's, hinting at content above and below also.

I used the following solution. Not the prettiest, but it seems to work. Requires UIScrollViewDelegate.
-(void)scrollViewDidScroll:(UIScrollView *)sender
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self scrollViewDidEndScrollingAnimation:subNavigation];
}
-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (scrollView.contentOffset.x <= 0) {
[self scrollViewReachedLeftBound];
} else if (scrollView.contentOffset.x >= 300) {
[self scrollViewReachedRightBound];
} else {
[leftArrow setHidden:NO];
[rightArrow setHidden:NO];
}
}
- (void)scrollViewReachedLeftBound
{
[leftArrow setHidden:YES];
[rightArrow setHidden:NO];
}
- (void)scrollViewReachedRightBound
{
[leftArrow setHidden:NO];
[rightArrow setHidden:YES];
}

Related

WKWebView zooming not working properly

I am using XWalkView which is a subclass of WKWebView, added as a subview to a container view programmatically. I am trying to zoom the content page of the WKWebView but it does not zoom first time. The WKWebView zooms the content page subsequently, i.e. when I pinch zoom or do double tap zoom it zooms WKWebView itself or container view (my guess) & after that, doing zoom gestures again the content page is zoomed (which is expected behaviour).
The question is that how to make the content page of WKWebView zoom whenever the zoom gesture is performed.
I am conforming to UIScrollViewDelegate protocol in my ViewController.h as below:
#interface ViewController : UIViewController<UIScrollViewDelegate, WKNavigationDelegate, WKUIDelegate>
I am setting the delegate to self in viewDidLoad as below:
xWalkView.scrollView.delegate = self;
Setting zooming scales in viewDidLoad as below:
xWalkView.scrollView.minimumZoomScale = 1;
xWalkView.scrollView.maximumZoomScale = 6;
& implementing the protocol methods in ViewController.m as below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
return xWalkView.scrollView.subviews[0];
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
}
I am using a BOOL shouldPageNotZoom & setting it YES or NO based on URL depending on whether zooming is required or not. On pages where zooming is required I can see the log of else block (NSLog(#"viewForZoomingInScrollView NO");) being printed in log area & on pages where zooming is not required I can see the log of if block (NSLog(#"viewForZoomingInScrollView YES");). Which is working. The only problem is content page does not zoom on first zooming gesture instead WKWebView or container view is zoomed (I guess), but zooms on subsequent zoom gestures.
After some observation I found that subview at index 0 of scrollview is being zoomed the first & subsequent times (but the content page zooms subsequent times not the first time), which is being returned in viewForZoomingInScrollView. Which should be the case, & is. I did this by assigning the tag values to each view & seeing the tag value by logging the tag value in scrollViewDidEndZooming method in log area as below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
xWalkView.scrollView.tag = 20;
xWalkView.tag = 15;
xWalkView.scrollView.subviews[0].tag = 10;
_containerView.tag = 5;
return xWalkView.scrollView.subviews[0];
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(#"tag %li", (long) view.tag);
}
Giving the below log:
2017-11-28 17:21:36.610753+0530 MyApp[4614:241659] tag 10 //Getting this 1st time of zooming
2017-11-28 17:21:59.369461+0530 MyApp[4614:241659] tag 10 //Getting this at subsequent zooms
I don't know what am I missing.
Please comment if any clarification is needed.
Got the solution! Returning the last subview first time & first subview of scrollview on subsequent zooms in viewForZoomingInScrollView method solved the problem, as shown below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
xWalkView.scrollView.tag = 20;
xWalkView.tag = 15;
xWalkView.scrollView.subviews[0].tag = 10;
_containerView.tag = 5;
if (isFirstTimeZooming) {
return xWalkView.scrollView.subviews[xWalkView.scrollView.subviews.count - 1];
} else {
return xWalkView.scrollView.subviews[0];
}
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(#"tag %li", (long) view.tag);
isFirstTimeZooming = NO;
}
isFirstTimeZooming is set to YES on load of the page in didStartProvisionalNavigation method & set to NO in scrollViewDidEndZooming method so that first subview of scrollview can be passed on subsequent zooming.
I had to use isFirstTimeZooming because if I pass last subview only in viewForZoomingInScrollView the zooming problem was reversed, i.e. it was zooming properly first time but not on subsequent zooms.

Interactive transition like Google Maps (iOS)

I want to achieve something very close to what Google Maps (iOS) does and I have some doubts.
But first, a better explanation and things to take into account:
-------------- ANSWER --------------
Thanks to Jugale's contribution, here's a repository so everybody can download and test everything out.
https://github.com/vCrespoP/VCSlidingView
-------------- ORIGINAL QUESTION -----------
You tap in a point inside the map, a view comes in from the bottom but then when you interact with that summary view:
Notice when pulling just a bit, the navigation bar already has set.
When you have scrolled it to the top, you can continue scrolling and the inner scrollview will continue scrolling.
When you 'reverse' the action to dismiss the view, the PanGesture doesn't mess up with the inner scrollView (same for the other way, scrollView VS dismiss)
Here it is in action:
Doubts:
I've tried to do it as an interactive transition (UIPercentDrivenInteractiveTransition) and separating Map from Details in 2 controllers but I'm having troubles with the UIPanGesture interfering with the scrollView.
Maybe it's better to do it as a subview and handle everything there? More or less like MBPullDownController (Although it has some issues with iOS8) -> https://github.com/matej/MBPullDownController
So, anybody knows any framework, has done it, or knows how to do this in a good way?
Thank you for your time :D
Looking through my implementation it seems the following are true:
I have a subclass of UIViewController that is the view controller
I have a subclass of UIView that is the overlay (and henceforth with the known as "the overlay") (actually for me this is a UIScrollView because it needs to go sideways too, but I'll try and filter out the unnecessary code)
I have another subclass of UIView that loads the overlay's content ("the content wrapper")
The content wrapper has a UIScrollView property, in which all other views are loaded ("the content view")
The view controller is responsible for initializing the overlay, setting it's initial frame (where the height is the height of the screen) and passing content to it, nothing more.
From it's -initWithFrame method, the overlay sets itself up with a UIDynamicItemBehavior. It also creates some UICollisionBehavior objects: one at the top of the screen and one below the bottom of the screen at just the right y position for the top of the overlay to be partially visible (as seen in the first frame of your GIF). A UIGravityBehavior is also set up to keep the overlay sitting on the lower collision boundary. Of course, _animator = [[UIDynamicAnimator alloc... is set up too.
Finally:
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan)];
_pan.delegate = self;
_pan.cancelsTouchesInView = FALSE;
The overlay class also has some other helpful methods such as changing the gravity's direction so that the overlay can appear to snap to the top or bottom of the screen.
The _pan handler uses a UISnapBehavior to keep the overlay moving dynamically up and down the screen underneath the user's finger:
- (void)handlePan
{
[self handlePanFromPanGestureRecogniser:_pan];
}
- (void)handlePanFromPanGestureRecogniser:(UIPanGestureRecognizer *)pan
{
CGFloat d = [pan velocityInView:self.superview.superview].y;
CGRect r = self.frame;
r.origin.y = r.origin.y + (d*0.057);
if (r.origin.y < 20)
{
r.origin.y = 20;
}
else if (r.origin.y > [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT)
{
r.origin.y = [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT;
}
if (pan.state == UIGestureRecognizerStateEnded)
{
[self panGestureEnded];
}
else if (pan.state == UIGestureRecognizerStateBegan)
{
[self snapToBottom];
[self removeGestureRecognizer:_tap];
}
else
{
[_animator removeBehavior:_findersnap];
_findersnap = [[UISnapBehavior alloc] initWithItem:self snapToPoint:CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r))];
[_animator addBehavior:_findersnap];
}
}
- (void)panGestureEnded
{
[_animator removeBehavior:_findersnap];
CGPoint vel = [_dynamicSelf linearVelocityForItem:self];
if (fabsf(vel.y) > 250.0)
{
if (vel.y < 0)
{
[self snapToTop];
}
else
{
[self snapToBottom];
}
}
else
{
if (self.frame.origin.y > (self.superview.bounds.size.height/2))
{
[self snapToBottom];
}
else
{
[self snapToTop];
}
}
}
The content wrapper listens for scroll events generated by the content view:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//this is our fancy way of getting the pan to work when the scrollview is in the way
if (scrollView.contentOffset.y <= 0 && _dragging)
{
_shouldForwardScrollEvents = TRUE;
}
if (_shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapper:isForwardingGestureRecogniserTouches:)])
{
[_delegate theContentWrapper:self isForwardingGestureRecogniserTouches:scrollView.panGestureRecognizer];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_dragging = FALSE;//scrollviewdidscroll must not be called after this
if (scrollView.contentOffset.y <= 0 || _shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapperStoppedBeingDragged:)])
{
[_delegate theContentWrapperStoppedBeingDragged:self];
}
}
_shouldForwardScrollEvents = FALSE;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_dragging = TRUE;
}
As you can see, when the bool shouldForwardScrollEvents is TRUE then we send scrollView.panGestureRecognizer to the content wrapper's delegate (the overlay). The overlay implements the delegate methods like so:
- (void)theContentWrapper:(TheContentWrapper *)contentWrapper isForwardingGestureRecogniserTouches:(UIPanGestureRecognizer *)contentViewPan
{
[self handlePanFromPanGestureRecogniser:contentViewPan];
}
- (void)theContentWrapperStoppedBeingDragged:(TheContentWrapper *)contentWrapper
{
//because the scrollview internal pan doesn't tell use when it's state == ENDED
[self panGestureEnded];
}
Hopefully at least some of this is useful to someone!

Make a UI scroll view scroll with a UIPageViewController

I am creating an app where I am trying to have a pagescrollviewcontroller swipe through screens and have the top "nav" bar titles scroll real time with the titles
i have the title bar view as a custom view and I am able to access the scroll delegate methods for both the custom scroll view and the pageview controller. However. I don't see how to access the real time scroll pos? I know it is possible because twitter does it (really can't shouldn't be in anyone's vocabulary) but I am not sure how to achieve this.
a picture
the home title swipes at the same scroll pos as the pagecontrollers.
current code:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if(scrollView.tag == 100) {
CGPoint point = CGPointMake(mainScrollView.contentOffset.x * 1,0);
[titleSwipe setContentOffset:point animated:YES];
}
else {
mainScrollView.contentOffset = CGPointMake(0,1);
[mainScrollView setContentOffset:titleSwipe.contentOffset animated:YES];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate: (BOOL)decelerate {
CGPoint point = CGPointMake(mainScrollView.contentOffset.x * 2,0);
[titleSwipe setContentOffset:point animated:YES];
}
You'll find the real time position in scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// X scroll
//
CGFloat percentage = scrollView.contentOffset.x / scrollView.contentSize.width;
NSLog(#"Scrolled percentage: %f", percentage);
}
Hope that helps!

How to scroll to a particular content offset directly(without showing other contents )

I am using
[scrollView_Mag setContentOffset:CGPointMake(320 * currentImageView, 0) animated:No];
so that I can scroll to a particular View(as required). In code "currentImageView" is the number of the view, I want to scroll.It is working perfectly. Only problem is, That for example i am 320*10 point and I want to setContent offset to 320*2, It scrolls from point 320*10 to 320*2, thus showing all other stuff. Which I dont want. I want It to scroll Immidately and without showing any other Content
[scrollView_Mag setHidden:YES];
[scrollView_Mag setContentOffset:CGPointMake(320 * currentImageView, 0) animated:NO];
[scrollView_Mag setHidden:NO];
When you move the content offset:
//Define this BOOL in header
isManuallyScrolling = YES;
[scrollView_Mag setHidden:YES];
[scrollView_Mag setContentOffset:CGPointMake(320 * currentImageView, 0) animated:NO];
//Use the scrollView delegate method to see when it stopped scrolling
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
//If we manually scrolled, unhide the scrollView
if (isManuallyScrolling) {
[scrollView_Mag setHidden:NO];
isManuallyScrolling = NO;
}
}

Scrollview wrong scrolling on custom control

I have a custom control that looks like this:
It allows you to pick multiplier from 1 to 10. Inside it is scroll view with multiple UILabels in it. Each label has tag and using the tag I calculate needed positions for label.
The user may either swipe to move this or tap on the number to make it move automatically.
But I have faced the problem - if the view is scrolling and I tap on the number while it is scrolling, it will stop not on the number I tapped on, but on some other number.
When the scroll view is not scrolling taps work fine. Here is relevant code:
This method is needed to stop on the number when it ends scrolling:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSInteger targetIndex = targetContentOffset->x / (self.frame.size.width / 4.0) ;
*targetContentOffset = CGPointMake((targetIndex) * (self.frame.size.width / 4.0), targetContentOffset->y);
if (targetIndex > [self.dataSource numberOfItemsInPicker:self] - 1) {
targetIndex = [self.dataSource numberOfItemsInPicker:self] - 1;
}
if (self.gameName) {
for (int i=0; i<self.labels.count; i++) {
UILabel *label = self.labels[i];
if (i == targetIndex) {
label.textColor = [GeneralHelper colorForGame:self.gameName];
}
else {
if ([self.delegate respondsToSelector:#selector(colorForTextForSmallNumberPicker:)]) {
label.textColor = [self.delegate colorForTextForSmallNumberPicker:self];
}
}
}
}
if (self.delegate) {
[self.delegate smallNumberPickerView:self didPickNumberAtIndex:targetIndex];
}
}
This is the code that determines position of labels in scroll view:
- (CGPoint)offsetForIndex:(NSUInteger)index
{
return CGPointMake(roundf((self.frame.size.width / 4.) * index), 0.);
}
And I think this is the offending code that handles tap gesture recognizer:
- (void)handleTapGesture:(UIGestureRecognizer *)recognizer
{
if (recognizer.state != UIGestureRecognizerStateEnded) {
return;
}
[self.scrollView setContentOffset:[self offsetForIndex:recognizer.view.tag - LABELS_START_TAG] animated:YES];
}
My ideas what happens is that during scrolling frame is wrong, or positions are wrong, but I have no idea how to handle this. I noticed that when I tap I NSLog target offset and it is wrong, and if it is not scrolling - I will get right offset.
What can I do about this?
There was an error in calculations. This line:
return CGPointMake(roundf((self.frame.size.width / 4.) * index), 0.);
It divided by 4 but the labels were actually smaller then 1/4 of the picker, so I got wrong results.

Resources