Detect direction of UIScrollView scroll in scrollViewWillBeginDragging - iphone

I did google enough, & I did check posts like these ( Finding the direction of scrolling in a UIScrollView? ) in stackoverflow before posting this. I have a dynamic number of photos in an iPhone App, that am displaying through UIScrollView. At any point in time, I have only 3 photos being displayed in the scroll-view. When I have, say 4 photos, in total:
1st photo : displayed at offset 0.0
2nd photo : displayed at offset 320.0
3rd photo : displayed at offset 640.0
Now, when the user scrolls to the 4th photo, the scroll-view resets to 0.0 offset. If the user tries to scroll 'beyond' the 4th photo, scrolling should stop in the right-direction only (so that user doesn't scroll 'beyond'). But currently, the user 'is able' to scroll beyond the last photo ; however, I detect this programmatically & reset the offset. But it doesn't look neat, as the user sees the black background momentarily. I want to detect that the user has started scrolling 'right' (remember, scrolling 'left' i.e. to the 'previous' photo is okay) in scrollViewWillBeginDragging, so that I can stop any further scrolling to the right.
What I tried:
Trying using self.scrollView.panGestureRecognizer's
translationInView isn't working, because there is no
panGestureRecognizer instance returned in the first place (!),
though the UIScrollView API claims so.
Detecting this in scrollViewDidEndDecelerating is possible, though
it'll not serve my purpose.

I had no issues determining direction in scrollViewWillBeginDragging when checking the scroll view's panGestureRecognizer:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer translationInView:scrollView.superview];
if(translation.y > 0)
{
// react to dragging down
} else
{
// react to dragging up
}
}
I found it very useful in canceling out of a scroll at the very first drag move when the user is dragging in a forbidden direction.

Swift 3 Solution
1- Add UIScrollViewDelegate
2- Add this code
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
let actualPosition = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if (actualPosition.y > 0){
// Dragging down
}else{
// Dragging up
}
}

For swift 2.0+ & ios 8.0+
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
let actualPosition = scrollView.panGestureRecognizer.translationInView(scrollView.superview)
if (actualPosition.y > 0){
// Dragging down
}else{
// Dragging up
}
}

Thank you, Kris. This is what worked for me, finally:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// Detect the scroll direction
if (lastContentOffset < (int)scrollView.contentOffset.x) {
...
}
}

This is what I used and it works nicely at least on iOS 6.0:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer translationInView:scrollView];
// Detect direction by accessing x or y of translation
}
Saves you the instance variable for lastContentOffset ...

Take a look at scrollViewWillEndDragging:withVelocity:targetContentOffset:.
You can use that method do do any checking and see if it is going where it should not and then in the method you can set a new targetContentOffset.
Per the documentation:
This method is not called when the value of the scroll view’s pagingEnabled property is YES. Your application can change the value of the targetContentOffset parameter to adjust where the scrollview finishes its scrolling animation.

There seem to be issues with detecting scroll direction based on the translation of the scrollView's pan recognizer in iOS 7+. This seems to be working pretty seamlessly for my purposes
func scrollViewDidScroll(scrollView: UIScrollView) {
if !scrollDirectionDetermined {
let translation = scrollView.panGestureRecognizer.translationInView(self.view)
if translation.y > 0 {
println("UP")
scrollDirectionDetermined = true
}
else if translation.y < 0 {
println("DOWN")
scrollDirectionDetermined = true
}
}
}
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
scrollDirectionDetermined = false
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
scrollDirectionDetermined = false
}

scrollView.panGestureRecognizer translationInView:scrollView doesn't report anything useful in scrollViewWillBeginDragging in iOS 7.
This does:
In the #interface
BOOL scrollDirectionDetermined;
and:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (!scrollDirectionDetermined) {
if ([scrollView.panGestureRecognizer translationInView:scrollView.superview].x > 0) {
//scrolling rightwards
} else {
//scrolling leftwards
}
scrollDirectionDetermined = YES;
}
}
and:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
scrollDirectionDetermined = NO;
}

Building off of #Oscar's answer, you can do things like
scrollView.bounces = actualPosition.y < 0 if you want the scrollView to bounce when you scroll to the bottom but not when you scroll to the top

Related

Swift PageControl larger dot on current page

I am attempting to scale dot of the current page to be larger than the dots which are not 'selected'.
I am using the scrollview delegate to ascertain which page is current.
At the moment there is no change to the size of the dot.
How would I go about achieving this?
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var i = 0
if(scrollView.contentOffset.x < scrollView.frame.width){
pageControl.currentPage = 0
pageControl.subviews.forEach {
if(i == 0){
print("Edit")
$0.transform.scaledBy(x: 5, y: 5)
}
pageControl.layoutIfNeeded()
i += 1
}
}else{
pageControl.currentPage = 1
}
}
UIPageControl is not that customizable. Use a library like this one
TAPageControl
Is works the same way as UIPageControl and you can easily customize current and non current page dots by setting the properties dotImage and currentDotImage.

Only allowing scrolling up in a Table View?

Is there a way to make it so the user can only scroll up in a table view? Right now I only see an option to scroll both up and down.
What does you mean under "scroll only up"?
If it's mean that content start from bottom and user can only scroll up, you can try this:
1) reverse array of objects that you use for feeding tableView
if shouldReverse {
var reversed = myObjects.reverse()
myObjects.removeAll(keepCapacity: false)
myObjects.splice(reversed, atIndex: 0)
}
2) implement scrollViewDidScroll: method from UIScrollViewDelegate protocol, and restrict content offset by it's y position
func scrollViewDidScroll(scrollView: UIScrollView) {
if shouldReverse && scrollView.contentOffset.y < 0 {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0)
}
}

iOS7 Status Bar like the native weather app

Does anyone know how can I reproduce a similar effect from the native iOS7 weather app?
Basically, the status bar inherits the view's background underneath, but the content doesn't show up.
Also, a 1 pixel line is drawn after the 20 pixels height of the status bar, only if some content is underlayed.
The best thing is to make it through the clipSubview of the view. You put your content into the view and make constraints to left/right/bottom and height. Height on scroll view you check is the cell has minus position and at that time you start to change the height of content (clip) view to get desired effect.
This is a real app you can download and take a look from www.fancyinteractive.com. This functionality will be available soon as next update.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *visibleCells = [convertorsTableView visibleCells];
if (visibleCells.count) {
for (CVConverterTableViewCell *cell in visibleCells) {
CGFloat positionYInView = [convertorsTableView convertRect:cell.frame toView:self.view].origin.y;
[self clipLayoutConstraint:cell.clipHeightLayoutConstraint withPosition:positionYInView defaultHeight:cell.frameHeight];
[cell.converterLabel layoutIfNeeded];
[cell.iconImageView layoutIfNeeded];
}
}
[self checkStatusBarSeperator:scrollView.contentOffset.y];
}
- (void)clipLayoutConstraint:(NSLayoutConstraint *)constraint withPosition:(CGFloat)position defaultHeight:(CGFloat)defaultHeight {
if (position < 0) {
constraint.constant = (defaultHeight - -position - 20 > 10) ? defaultHeight - -position - 20 : 10;
} else
constraint.constant = defaultHeight;
}
You can accomplish this by setting a mask to the table view's layer. You will not be able however to render the animations inside the cells, but you can do those yourself behind the table view, and track their movement with the table view's scrollview delegate methods.
Here is some informations on CALayer masks:
http://evandavis.me/blog/2013/2/13/getting-creative-with-calayer-masks
Swift 5:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visibleCells = tableView.visibleCells as? [TableViewCell] else { return }
let defaultClipHeight: CGFloat = 24
let statusBarHeight: CGFloat = UIApplication.statusBarHeight
if !visibleCells.isEmpty {
for cell in visibleCells {
let topSpace = cell.frame.size.height - defaultClipHeight - cell.clipBottomConstraint.constant
let cellOffsetY = tableView.contentOffset.y - cell.frame.origin.y + statusBarHeight
if cellOffsetY > topSpace {
let clipOffsetY = cellOffsetY - topSpace
let clipHeight = defaultClipHeight - clipOffsetY
cell.clipHeightConstraint.constant = max(clipHeight, 0)
} else {
cell.clipHeightConstraint.constant = defaultClipHeight
}
}
}
}
Starting Page:
Scrolling First Item:
Scrolling Second Item:

UICollectionView: paging like Safari tabs or App Store search

I want to implement "cards" in my app like Safari tabs or App Store search.
I will show user one card in a center of screen and part of previous and next cards at left and right sides. (See App Store search or Safari tabs for example)
I decided to use UICollectionView, and I need to change page size (didn't find how) or implement own layout subclass (don't know how)?
Any help, please?
Below is the simplest way that I've found to get this effect. It involves your collection view and an extra secret scroll view.
Set up your collection views
Set up your collection view and all its data source methods.
Frame the collection view; it should span the full width that you want to be visible.
Set the collection view's contentInset:
_collectionView.contentInset = UIEdgeInsetsMake(0, (self.view.frame.size.width-pageSize)/2, 0, (self.view.frame.size.width-pageSize)/2);
This helps offset the cells properly.
Set up your secret scrollview
Create a scrollview, place it wherever you like. You can set it to hidden if you like.
Set the size of the scrollview's bounds to the desired size of your page.
Set yourself as the delegate of the scrollview.
Set its contentSize to the expected content size of your collection view.
Move your gesture recognizer
Add the secret scrollview's gesture recognizer to the collection view, and disable the collection view's gesture recognizer:
[_collectionView addGestureRecognizer:_secretScrollView.panGestureRecognizer];
_collectionView.panGestureRecognizer.enabled = NO;
Delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.x = contentOffset.x - _collectionView.contentInset.left;
_collectionView.contentOffset = contentOffset;
}
As the scrollview moves, get its offset and set it to the offset of the collection view.
I blogged about this here, so check this link for updates: http://khanlou.com/2013/04/paging-a-overflowing-collection-view/
You can subclass UICollectionViewFlowLayout and override like so:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)contentOffset
withScrollingVelocity:(CGPoint)velocity
{
NSArray* layoutAttributesArray =
[self layoutAttributesForElementsInRect:self.collectionView.bounds];
if(layoutAttributesArray.count == 0)
return contentOffset;
UICollectionViewLayoutAttributes* candidate =
layoutAttributesArray.firstObject;
for (UICollectionViewLayoutAttributes* layoutAttributes in layoutAttributesArray)
{
if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell)
continue;
if((velocity.x > 0.0 && layoutAttributes.center.x > candidate.center.x) ||
(velocity.x <= 0.0 && layoutAttributes.center.x < candidate.center.x))
candidate = layoutAttributes;
}
return CGPointMake(candidate.center.x - self.collectionView.bounds.size.width * 0.5f, contentOffset.y);
}
This will get the next or previous cell depending on the velocity... it will not snap on the current cell however.
#Mike M's answer in Swift…
class CenteringFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView,
let layoutAttributesArray = layoutAttributesForElementsInRect(collectionView.bounds),
var candidate = layoutAttributesArray.first else { return proposedContentOffset }
layoutAttributesArray.filter({$0.representedElementCategory == .Cell }).forEach { layoutAttributes in
if (velocity.x > 0 && layoutAttributes.center.x > candidate.center.x) ||
(velocity.x <= 0 && layoutAttributes.center.x < candidate.center.x) {
candidate = layoutAttributes
}
}
return CGPoint(x: candidate.center.x - collectionView.bounds.width / 2, y: proposedContentOffset.y)
}
}
A little edit on Soroush answer, which did the trick for me.
The only edit I made instead of disabling the gesture:
[_collectionView addGestureRecognizer:_secretScrollView.panGestureRecognizer];
_collectionView.panGestureRecognizer.enabled = NO;
I disabled scrolling on the collectionview:
_collectionView.scrollEnabled = NO;
As disabling the gesture disabled the secret scrollview gesture as well.
I'll add another solution. The snapping in place is not perfect (not as good as when paging enabled is set, but works well enough).
I have tried implementing Soroush's solution, but it doesn't work for me.
Because the UICollectionView is a subclass of UIScrollView it will respond to an important UIScrollViewDelegate method which is:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
The targetContentOffset (an inout pointer) lets you redefine the stopping point for a collection view (the x in this case if you swipe horizontally).
A quick note about a couple of the variables found below:
self.cellWidth – this is your collection view cell's width (you can even hardcode it there if you want)
self.minimumLineSpacing – this is the minimum line spacing you set between the cells
self.scrollingObjects is the array of objects contained in the collection view (I need this mostly for the count, to know when to stop scrolling)
So, the idea is to implement this method in the collection view's delegate, like so:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (self.currentIndex == 0 && velocity.x < 0) {
// we have reached the first user and we're trying to go back
return;
}
if (self.currentIndex == (self.scrollingObjects.count - 1) && velocity.x > 0) {
// we have reached the last user and we're trying to go forward
return;
}
if (velocity.x < 0) {
// swiping from left to right (going left; going back)
self.currentIndex--;
} else {
// swiping from right to left (going right; going forward)
self.currentIndex++;
}
float xPositionToStop = 0;
if (self.currentIndex == 0) {
// first row
xPositionToStop = 0;
} else {
// one of the next ones
xPositionToStop = self.currentIndex * self.cellWidth + (self.currentIndex + 1) * self.minimumLineSpacing - ((scrollView.bounds.size.width - 2*self.minimumLineSpacing - self.cellWidth)/2);
}
targetContentOffset->x = xPositionToStop;
NSLog(#"Point of stopping: %#", NSStringFromCGPoint(*targetContentOffset));
}
Looking forward to any feedback you may have which makes the snapping in place better. I'll also keep on looking for a better solution...
The previous is quite complicated, UICollectionView is a subclass of UIScrollView, so just do this:
[self.collectionView setPagingEnabled:YES];
You are all set to go.
See this detailed tutorial.

What's the best way of handling a UIPageControl that has so many dots they don't all fit on the screen

I have a UIPageControl in my application that looks perfectly fine with around 10 pages (dots), it's possible however for the user to add many different views, and so the number of dots could become say 30.
When this happens the dots just disappear off the edge of the screen, and you can't always see the currently selected page, making it all look terrible.
Is there any way to make the pagecontrol multi-line, or to shift it left or right at the moment the currently visible page disappears of the screen.
I created an eBook application that used UIScrollView that contained a UIWebView. The book had over 100 pages so UIPageControl could not handle it because of the problem you pointed out. I ended up creating a custom "slider" type view that acted similar to UIPageControl but could handle the large number of pages. That's pretty much what you will need to do. UIPage control cannot handle as many pages as you want...
Calculate the pages/dot ratio and store as float value. Then calculate current dot number as current page/pagesPerDot. You may need to advance a few pages before seeing the dot move, but it's very scalable and works well.
I had to implement UIPageControl under my horizontal collection view which had 30 items. So what i did to reduce the number of dots by fixing the number of dots and scroll in those dots only. Follow the code:
#IBOutlet var pageControl: UIPageControl!
var collectionDataList = [String]
let dotsCount = 5
override func viewDidAppear(_ animated: Bool) {
if collectionDataList.count < dotsCount {
self.pageControl.numberOfPages = collectionDataList.count
} else {
self.pageControl.numberOfPages = dotsCount
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let pageWidth = scrollView.frame.width
self.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
self.pageControl.currentPage = self.currentPage % dotsCount
}
My solution is setting the maximum number of dots and showing the dot before the last one until the user reaches the end. If the current page number is bigger than maximum dot count we show the one before the last one until the user reaches the end.
var maxDotCount = 8 //Global constant.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = collectionView.frame.size.width
let currentPage = collectionView.contentOffset.x / pageWidth
let pageControlsNumberOfPages = pageControl.numberOfPages - 1
let dataSourceCount = CGFloat(dataSourceArr.count - 1)
let maxDotCountReduced = CGFloat(maxDotCount - 1)
if (dataSourceCount > maxDotCountReduced) {
if (currentPage >= maxDotCountReduced) {
if (currentPage == dataSourceCount) {
pageControl.currentPage = pageControlsNumberOfPages
} else {
pageControl.currentPage = pageControlsNumberOfPages - 1
}
return
}
}
pageControl.currentPage = Int(currentPage)
}
I set the maximum number of pages to 20 (the most that will fit on the iPhone 5S/SE in Portrait Mode).. and if I am on a page over 20, I keep the dot in the same place, but make the dot red. I think people realise what it means, I often have a numeric page number on screen too, and First and Last buttons too, and I feel people are more interested in seeing the dot move on the initial pages.