using scrollViewWillEndDragging to ensure UIScrollView stops at defined increments - ios

I would like my UIScrollView to naturally glide to endings at certain incremental values, corresponding to every 50 points of width of a horizontally-scrolling UIScrollView To do this, I customed scrollViewWillEndDragging, like so (as recommended, but not described in detail in an answer here Scrolling a horizontal UIScrollView in stepped increments?):
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if(fmodf(targetContentOffset->x, 50.0)){
int roundingNumber1 = 50;
CGFloat newOffset = roundingNumber1 * floor(((scrollView.contentOffset.x)/roundingNumber1)+0.5);
targetContentOffset->x = newOffset;
}
}
However, I am not sure this is actually stopping the view at specific increments, and I also notice that the effect is asymmetric. Though my scrolling motions/velocity/etc are the same, scrolling right is much less fluid than scrolling left. Scrolling right stops faster and more abruptly. There's a video here. Why is this behavior asymmetric and how can I change it?
The reason I thin the scrolling is not stopping at increments of 50 is that I also have a UILabel underneath the scroll view that gets updated by other delegate functions to show the offset. The value it shows is rarely close to 50 when the scrolling is done. Is this because the scrolling is not incrementing to values of 50 or because I am not updating at the right times?
Thanks for any advice.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView{
if(abs(self.lastOffset - scrollView.contentOffset.x) > 49){
CGFloat newNumber = scrollView.contentOffset.x;
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
self.lastOffset = scrollView.contentOffset.x;
[self.view setNeedsDisplay];
}
}
- (void) scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
}

Rather than implementing these delegate methods, you can set pagingEnabled on your scroll view to true to get this behavior for free. When paging is enabled, the scroll view will automatically snap to the nearest "page" when you end dragging, where the page width is equal to the scroll view's width.
Based on your video, it looks like the items in your scroll view are smaller than the width of the scroll view itself. To use paging, you'll have to do the following:
Make your scroll view have the same width as one of your items (50.0 units in your case).
Set scrollView.clipsToBounds to false so that the scroll view draws subviews outside of its much smaller bounds.
Set scrollView.pagingEnabled to true so that the scroll view scrolls with paging.
At this point, paging will work but you won't be able to drag the scroll view outside of its bounds. To make this work, you'll need to embed the scroll view in a larger view that forwards touch events to it.
Create a "touch forwarding" class and add it to your view.
This class takes all touch events it receives and sends them to its targetView property instead. DJK is a random prefix I made up for the class name.
#interface DJKTouchForwardingView : UIView
/** The view to which touch events should be forwarded. */
#property (weak, nonatomic) UIView *targetView;
#end
#implementation DJKTouchForwardingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *child = nil;
if ((child = [super hitTest:point withEvent:event]) == self) {
return self.targetView;
}
return child;
}
#end
Make the scroll view a subview of the touch forwarding view and assign it to the targetView property.
Your scroll view will now be scrollable within the bounds of the touch forwarding view and will snap to 50 unit pages.

Related

How to animate views according to tableview scrolling directions without content scrolling

i'm trying to implement a specific tableview behaviour (Like on Facebook app).
I want to have a dynamic header that will be magnified every time the user scrolls up and will be shrieked when the user scroll down.
In addition i want the tableview to cause the effect of pushing the header and than scrolling the tableview cells.
I used the method:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
in this method i calculated the offset and the direction and called a method that shrink or magnify the header accordingly
so far so good.
The thing is that the animation is being performed with the tableview scrolling.
To avoid it, I created a custom scrollview on to of the top of my tableview, I taged the two scrollviews differently.
In the scrollview i created a weak reference of the tableview and a boolean value that indicated if the scrollview should return the tableview touch.
When the shrinking\magnifying animation was finished i changed the boolean value so it will signal the custom scrollview to return the tableview in my HitTest methods that i implemented inside the scrollview.
But hitTest not called when the user keep scrolling (without leafing the finger), in additions now my buttons inside my tableViewCell aren't reacting.
Here is my HitTest Method:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if (_recieveTouchOnTable)
{
return _table;
}
else
return result;
}
Here is my scrollViewDidScroll method:
(onProgress means that the animation is being performed, so keep returning the custom scrollview)
Tag = 2 = the custom scrollview
Tag = 1 = the tableview
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGFloat yVelocity = [aScrollView.panGestureRecognizer velocityInView:aScrollView].y;
CGFloat offset = lastCustomScrollViewContentOffset.y-aScrollView.contentOffset.y;
lastCustomScrollViewContentOffset =aScrollView.contentOffset;
if (yVelocity<0)
offset = fabs(offset)*-1;
else if(yVelocity>0)
offset = fabs(offset);
if (offset!=0 && aScrollView.tag == 2)
[self layoutViewAccorrdingToTableviewScorlingVelocity:offset];
if (!onProgress ){
customScrollView.recieveTouchOnTable=YES;
}
}
Am i missing something, or maybe there's a more simple way to do it?

Swipe UITableView onto screen, keep swiping?

I want a tableview that starts offscreen and can scroll on-screen, reach the top, and keep scrolling. I've made a visual of the desired interaction below.
I've tried two things, and neither work exactly like I need.
The first thing I did was put the tableview in a scrollview, and move the scrollview when panning is detected on the tableview. This blocks touches in the tableview, and even if I could detect when the tableview hit the top of the screen, I'm not sure how I would continue scrolling.
The second thing I tried was to set the content size of the scrollview to the height of the tableview. This lets the tableview scroll, but I only seem to be able to receive touches in the initial small rectangle labeled "List Item 1". As the tableview scrolls, I can't grab the middle and scroll it anymore.
What's the best way to build this interaction? Edit: A map surrounds this bottom view to the left, right, and mostly top. When the bottom view is pulled up, the map is visible to the left and right.
1.)
2.)
3.) (and this keeps scrolling for as many items are as in the list.)
I guess you want something like this:
or this:
I laid out my table view over my map view. I set the table view's contentInset and contentOffset like this:
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = 44;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.tableView.contentInset = (UIEdgeInsets){ .top = self.view.bounds.size.height - self.tableView.rowHeight };
self.tableView.contentOffset = CGPointMake(0, -self.tableView.contentInset.top);
}
Note that, although the default row height is 44, tableView.rowHeight return -1 unless you explicitly set it. (Setting it to 44 in the storyboard doesn't change this.)
I used a subclass of UITableView in which I did two things:
I explicitly set self.backgroundColor = [UIColor clearColor]. I found that setting the background color to clear in the storyboard didn't work.
I overrode pointInside:withEvent::
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return point.y >= 0 && [super pointInside:point withEvent:event];
}
Note that you don't care about contentInset here. The table view's contentOffset.y (which is the same as its bounds.origin.y) is set to a negative number when its top content inset is exposed. It's set to 0 when the top of item 0 is at the top edge of the table view, which isn't the case when the item as at the bottom edge of the screen.
Another thing you might want is to prevent the table from stopping half-on the screen. If the user drags item 0 halfway up the screen, you want the table to scroll so item 0 is all the way at the top of the screen (if there are sufficient items), and if the user drags item 0 halfway down the screen, you want the table to scroll so just item 0 is showing.
I did that by making my view controller act as the table view's delegate and implementing this delegate method, inherited from UIScrollViewDelegate:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat yMin = -self.tableView.contentInset.top;
CGFloat yMax = MIN(0, self.tableView.contentSize.height - self.tableView.bounds.size.height);
if (targetContentOffset->y < yMax) {
if (velocity.y < 0) {
targetContentOffset->y = yMin;
} else {
targetContentOffset->y = yMax;
}
}
}
That method is carefully written so that it works for tables too short to fill the screen vertically, and for tables that can fill the screen vertically.
I've uploaded my test project here: https://github.com/mayoff/tableView-over-mapview
Update for side-by-side tables
I don't think side-by-side tables is going to be a good user interface. I think it's going to be confusing. But here's how you do it.
The view hierarchy looks like this:
Root view
MKMapView
MyScrollView
ScrollContentView
MyTableView for first table
MyTableView for second table
MyTableView for third table
etc.
The map view and the scroll view have the same frames. The scroll view handles the sideways scrolling and each table view is independently scrollable vertically.
Since the scroll view should only capture touches that land in one of the table views, it needs a custom hitTest:withEvent: that returns nil for touches outside any of the table views:
#implementation MyScrollView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
#end
But this won't actually do the job, because (in my implementation) the scroll view has just one big subview, the ScrollContentView. So we need to do the same thing in ScrollContentView:
#implementation ScrollContentView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
That's sufficient to pass touches down to the map view if they land outside of the tables.
I also use ScrollContentView to lay out the tables and set the scroll view's content size:
- (void)layoutSubviews {
[super layoutSubviews];
// Layout of subviews horizontally:
// [gutter/2][gutter][subview][gutter][subview][gutter][subview][gutter][gutter/2]
// where 3 * gutter + subview = width of superview
CGSize superSize = self.superview.bounds.size;
CGFloat x = kGutterWidth * 3 / 2;
CGFloat subWidth = superSize.width - kGutterWidth * 3;
for (UITableView *subview in self.subviews) {
subview.frame = CGRectMake(x, 0, subWidth, superSize.height);
x += subWidth + kGutterWidth;
CGFloat topInset = superSize.height - subview.rowHeight;
subview.contentInset = (UIEdgeInsets){ .top = topInset };
subview.contentOffset = CGPointMake(0, -topInset);
}
x += kGutterWidth / 2;
self.frame = CGRectMake(0, 0, x, superSize.height);
((UIScrollView *)self.superview).contentSize = self.bounds.size;
_pageWidth = subWidth + kGutterWidth;
}
I also made my view controller be the scroll view's delegate, and implemented a delegate method to force the scroll view to stop on “page” (table) boundaries:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat pageWidth = contentView.pageWidth;
// Force scroll view to stop on a page boundary.
CGFloat pageNumber = targetContentOffset->x / pageWidth;
if (velocity.x < 0) {
pageNumber = floor(pageNumber);
} else {
pageNumber = ceil(pageNumber);
}
pageNumber = MAX(0, MIN(pageNumber, contentView.subviews.count - 1));
targetContentOffset->x = pageNumber * pageWidth;
}
The result:
I've updated the git repository with this version.
You ought to be able to do this pretty easily by setting your table view’s top contentInset to something high (as sha suggested in the comments) and then making your UITableView a subclass so you can override -pointInside:withEvent:. Using that and the current contentOffset, you can determine whether the incoming event is inside the area you want to be scrollable, and return YES or NO accordingly; if you return NO, then the touch should fall through to the map view as intended.
Why not change this completely. You said you have a map "underneath" the tableview. So when scrolled up the map will be hidden over by the table view. I presume when you scroll down again the map will be revealed?
You should be able to do this by using the UITableView header. Either a section header or a table view header. They behave slightly differently upon scrolling.
I'd maybe do it this way...
Use a table view header on the table. In this header you place your map view.
By default this will be pinned to the top of the table so if you scroll the table up then the map will slide off the top of the screen with it.
However, if you then intercept the scroll view delegate method - (void)scrollViewDidScroll:(UIScrollView *)scrollView; then you can work out if the table is being scrolled upwards and offset the map view so it stays where it is.
i.e. if the table is scrolled to (0, 10) then offset the map to (0, -10) so it looks like it hasn't moved.
This will give you the scroll in and out feature of the tableview and keep the map in view and responding to touches.

ScrollviewDidScroll not called at each point

There is a button at the bottom of my view controller. When the user scrolls down the button has to be attached to the scrollview at certain height.
I need to attach a button to the scrollview, immediately when the contentOffset.y reaches a particular value. -(void) scrollviewDidScroll doesn't help me as there might be a jump in contentOffset when the user is scrolling fast. Any leads on this are helpful.
Also, whenever I add a subview to the scrollview, -(void) viewDidLayoutSubviews is called. Which in turn sets the contentOffset to {0,0}. How can I achieve the functionality I need?
I needed to do the same thing with a UITableView and for me using scrollViewDidScroll worked.
I created a view called staticBar and added it as a subview of the tableView, but I had to rearrange the tableview subviews for it to appear in the right place. I don't have my code in front of me, but in -scrollViewDidScroll: it looked something like this:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
CGFloat staticBarAdjustedY = _staticBarY - scrollView.contentOffset.y;
CGFloat scrollViewYFloor = scrollView.frame.size.height - _staticBar.frame.size.height;
// This way maximum Y the view can have is at the base of the scrollView
CGFloat newY = MIN( staticBarAdjustedY, scrollViewYFloor);
_staticBar.frame = (CGRect){ { _staticBar.frame.origin.x, newY}, _staticBar.frame.size}
}
I will check my code later today and add more details here.
Also, you said the scrollviewDidScroll has jumps in contentOffset, but it's worth mentioning that these jumps are the same that the scrollView uses to scroll its own view. So it's not like you are "losing" frames on this delegate method.
Hope it helps.
PS: So, here is the rest of my code.
//I place my custom view as a subview of the tableView below it's last subview
//The last subview is for scroll indicators.
WTButtonsBar *buttonBar = [[WTButtonsBar alloc] init];
[self.tableView insertSubview:buttonBar belowSubview:self.tableView.subviews.lastObject];
In scrollViewDidScroll:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//In my app I needed my view to stick to the top of the screen
//thats why I use MAX here
//self.buttonsBarOriginalY is the view's position in the scrollView when it isn't attached to the top.
CGFloat newY = MAX(scrollView.contentOffset.y, self.buttonsBarOriginalY)
[_buttonsBar setFrame:(CGRect){{0, newY}, _buttonsBar.frame.size}];
}

UIScrollView inside UIPageViewController page scrolling issue

I have a situation - a UIPageViewController that has scrolling direction vertical. All of its pages are the same height as itself. And then inside each page I have a UIScrollView that also has scrolling in vertical direction.
The point of such an architecture is because i have news with some text between which i want to be able to switch with paging effect, but the news text can be bigger than the screen.
Now i got it all working good with one tiny issue. When you scroll the scrollView to the bottom it bounces (if you turn bouncing on) or just stops (if you turn bouncing off). And only after you lift your finger (stop the touches) you are able to switch to the next page in UIPageViewController.
What i would like to see is when you scroll to the bottom of the UIScrollView it automatically starts scrolling the UIPageViewController without you having to lift up your finger.
I expected that to be the natural behaviour. But its not in ios6 only in 7.
https://www.dropbox.com/s/noua5jo5trv5kn1/iOS%20Simulator%20Screen%20shot%20Sep%2029%2C%202013%207.08.03%20PM.png
https://www.dropbox.com/s/m0965bff5h4eq21/iOS%20Simulator%20Screen%20shot%20Sep%2029%2C%202013%207.08.11%20PM.png
implement UIScrollViewDelegate imside your UIViewConroller
something like
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y == scrollView.contentSize.height) {
if (self.nextPageNeededHandler) {
self.nextPageNeedeHandler();
}
}
}
and block in your .h file
#property (nonatomic, copy) void (^nextPageNeedeHandler)(void);
set property like
...
__weak MyPageViewController *self_ = self;
nextPageVC.nextPageNeedeHandler = ^{
[self_ goNextPage];
};
...

UIScrollView with fixed contents inside

my question is:
In my app I want realize somethings like this using UIScrollView
A UIScrollView whit all his contents inside. But when I reached the top of the screen (or maybe the bottom of the screen) I want to anchor some contents like in the AppStore (iOS 6).
In this case the "Detail", "Review" and "Related" buttons stop scrolling and remains fixed at the top.
Anyone have any idea how to do?
Give your scroll view a delegate if it doesn't already have one. This would probably be your view controller.
Also give the delegate a reference to the floating view. (In the example, the floating view contains the “Dettagli”, “Recensioni”, and “Correlati” buttons, and the floating view is transparent where the triangular notch is, and the shadow is also part of the floating view.) The floating view must be a subview of the scroll view.
Save the “normal” Y coordinate of the floating view's frame in an instance variable. For example, if the delegate is a view controller, you should probably do it in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
originalFloatingViewY = floatingView.frame.origin.y;
}
In the delegate's scrollViewDidScroll: method, check whether the scroll view has been scrolled below the top of the “normal” position of the floating view. If so, move the floating view to the top edge of the visible area:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self updateFloatingViewFrame];
}
- (void)updateFloatingViewFrame {
CGPoint contentOffset = scrollView.contentOffset;
// The floating view should be at its original position or at top of
// the visible area, whichever is lower.
CGFloat y = MAX(contentOffset.y, originalFloatingViewY);
CGRect frame = floatingView.frame;
if (y != frame.origin.y) {
frame.origin.y = y;
floatingView.frame = frame;
}
}
You could also do it by subclassing UIScrollView and overriding layoutSubviews. Watch the “Advanced ScrollView Techniques” video from WWDC 2011 for more information on this technique.
You need to handle this in the UIScrollView's delegate method( scrollViewDidScroll:) for scrolling. When the y of scrollview reaches that particular value you need to disable scrolling in this direction alone. You just need to add an if condition there to check and reset the y point while scrolling. For this you might have to keep this particular portion as a separate scrollview outside the below content and move it along with the below scroll view's upward movement.

Resources