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.
Related
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!
I have Scrollview using for displaying images along with two buttons previous and next.
Now my problem is when the user is scrolling the image I want to identify whether it is from right to left scroll or vice versa. For this purpose I have to calculate scrollview contentOffset and scrollview frame size but I don't know how to calculate those values.
Still now I used:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0){}
this method.
need any suggestions.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView1 willDecelerate:(BOOL)decelerate;
{
CGFloat pageWidth = scrollView.frame.size.width;
float fractionalPage = scrollView.contentOffset.x / pageWidth;
NSInteger page = lround(fractionalPage);
if (previousPage != page){
if ([scrollView.panGestureRecognizer translationInView:scrollView.superview].x > 0){
NSLog(#"-1");
} else {
NSLog(#"+1");
}
}
}
Hi if you are scrolling Horizontally you will get to know whether you are going next or previous depending upon velocity.x
velocity.x > 0 ?(int) _currentIndex + 1 :(int) _currentIndex - 1;
-(void) scrollViewWillBeginDragging:(UIScrollView *)scrollView {
CGFloat xAxisVisible= self.uiScrollDetails.contentOffset.x;
self.currentIndex=xAxisVisible/320;
NSLog(#"Dragging - You are now on page %i",(int) self.currentIndex);
}
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!
I've found a good solution for looping my scrollview, but I'm also using a UIPageControl for showing what the actual page's number is.
I change the pageControl.currentPage property when the scrollview scrolls, so if you scroll the scrollview more than a half the pageControl shows the new page.
I'd like to keep this feature, but with my actual code the pageNumber is not correct.
I think I should use this: https://github.com/gblancogarcia/GBInfiniteScrollView
But I can't make it work...
Here's my code:
-(void)rotateIfNecessary{
if(scrollView.contentOffset.x == 0) {
CGPoint newOffset = CGPointMake(scrollView.bounds.size.width+scrollView.contentOffset.x, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsRight];
}
else if(scrollView.contentOffset.x == (scrollView.contentSize.width - scrollView.frame.size.width) ) {
CGPoint newOffset = CGPointMake(scrollView.contentOffset.x-scrollView.bounds.size.width, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsLeft];
}
}
-(void)rotateViewsRight {
NSMutableArray* controllers = [DashboardFrameUtil getFrameViewControllers];
DashboardFrameViewController *endView = [controllers lastObject];
[controllers removeLastObject];
[controllers insertObject:endView atIndex:0];
[self resizePages];
}
-(void)rotateViewsLeft {
NSMutableArray* controllers = [DashboardFrameUtil getFrameViewControllers];
DashboardFrameViewController *endView = [controllers firstObject];
[controllers removeObjectAtIndex:0];
[controllers addObject:endView];
[self resizePages];
}
Then I call rotateIfNecessary in my scrollViewDidEndDecelerating, everything works fine, but the page calculating.. (when the scrollview resizes itself, it calls the scrollViewDidScroll where I calculate the pageNumber:
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
pageControl.currentPage = page;
The pageControl doesn't show the correct page number when the scrollView loops.
Any idea?
Thank you in advance!
Let's start with:
if(scrollView.contentOffset.x == 0) {
CGPoint newOffset = CGPointMake(scrollView.bounds.size.width+scrollView.contentOffset.x, scrollView.contentOffset.y);
...
}
condition is true when scrollView.contentOffset.x == 0, so what for do you add scrollView.contentOffset.x (it's surely = 0) to scrollView.bounds.size.width ?
UPDATED
I didn't run code - there can be some mistakes
If I understand correctly when you, for example, rotate right - last element became first and you want LAST page to be showen?
If yes -- the problem with pages is because you change contentOffset all the time and also change 'data source' of scroll view, so there's no correlation between contentOffset and individual DashboardFrameViewController - same DashboardFrameViewController can be at different offsets in different moments of time. So You have to calculate page N according to something else.
For example:
Somewhere in ivar store initial controllers (with initial order):
- (void)viewDidLoad {
[super viewDidLoad];
_initialControllers = [[DashboardFrameUtil getFrameViewControllers] copy];
}
then you need some method to discover what vc is currently vissible or nearest to center (and store in ivar _currentlyVissibleVC), for example:
- (DashboardFrameViewController *)vissibleVC {
for (DashboardFrameViewController *vc in _initialControllers){
if (CGRectContainsPoint(vc.frame, CGPointMake(scrollView.contentOffset.x + scrollView.bounds.size.width/2, scrollView.contentOffset)){
_currentlyVissibleVC = vc;
return;
}
}
}
then you can calculate page num:
int pageN = [_initialControllers indexOfObject:_currentlyVissibleVC] + 1;
Hope helps
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];
}