UIScrollView - showing the scroll bar - iphone

Possibly a simple one!
Does anyone know how to get the scroll bar of a UIScrollView to constantly show?
It displays when the user is scrolling, so they can see what position of the scroll view they are in.
BUT I would like it to constantly show because it is not immediately obvious to the user that scrolling is available
Any advice would be highly appreciated.

No, you can't make them always show, but you can make them temporarily flash.
[myScrollView flashScrollIndicators];
They are scroll indicators, not scroll bars. You can't use them to scroll.

my solution for show scroll indicators all the time
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
UPDATE: This solution cause some issues on 64-bit. For more detail look here

As far as I know, this isn't possible. The only API call which controls displaying the scroll indicator is showsVerticalScrollIndicator and that can only disable displaying the indicator altogether.
You could flashScrollIndicators when the view appears so that the user knows where in the scroll view they are.

This one worked for me:
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
I got this snippet from here: http://www.developers-life.com/scrollview-with-scrolls-indicators-which-are-shown-all-the-time.html

Swift 3+
1) Timer
var timerForShowScrollIndicator: Timer?
2) Methods
/// Show always scroll indicator in table view
func showScrollIndicatorsInContacts() {
UIView.animate(withDuration: 0.001) {
self.tableView.flashScrollIndicators()
}
}
/// Start timer for always show scroll indicator in table view
func startTimerForShowScrollIndicator() {
self.timerForShowScrollIndicator = Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(self.showScrollIndicatorsInContacts), userInfo: nil, repeats: true)
}
/// Stop timer for always show scroll indicator in table view
func stopTimerForShowScrollIndicator() {
self.timerForShowScrollIndicator?.invalidate()
self.timerForShowScrollIndicator = nil
}
3) Use
startTimerForShowScrollIndicator in viewDidAppear
stopTimerForShowScrollIndicator in viewDidDisappear

I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector).
I use swizzling instead. And also I don't need to use tags.
Add this method to your view controller, where you have scroll view (self.categoriesTableView property is a table view where I want to show scroll bars)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with vertical scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Search correct subview with horizontal scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (view.frame.size.height < 10 && view.frame.size.height < view.frame.size.width) {
if (self.categoriesTableView.frame.size.width < self.categoriesTableView.contentSize.width) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
The scroll indicator (vertical scroll indicator in first for cycle and horizontal in second for cycle) will be always at the screen. If you need only one indicator, left only this for cycle in code and remove another one.

For webviews, where the first subview is a scrollview, in the latest SDK, if an HTML page is longer than the frame, no scroll bar is shown, and if the html content happens to line up with the frame, or you have a whitespace at the bottom of the frame, it 'looks' like there is no scroll needed and nothing below the line. In this case, I think you should definately flash the scroll bars in the delegate's
- (void)webViewDidFinishLoad:(UIWebView *)webView;
method to alert the user that there is more stuff 'outside the box'.
NSArray *subViews = [[NSArray alloc] initWithArray:[webView subviews]] ;
UIScrollView *webScroller = (UIScrollView *)[subViews objectAtIndex:0] ;
With HTML, the horizontal content is wrapped automatically, so check the webscroller height.
if (webScroller.contentSize.height > webView.frame.size.height) {
[webScroller flashScrollIndicators];
}
The flash is so short, and happens while over views are loading, that it can be overlooked. To work around that, you could also jiggle or bounce or scroll or scale the content a little via the generic UIView commitAnimations

iOS does not offer the API. But if you really want this, you can add your custom indicator to scroll view and layout it yourself, just as the demo does:
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.showsVerticalScrollIndicatorAlways) {
scroll_indicator_position(self, k_scroll_indicator_vertical);
}
if (self.showsHorizontalScrollIndicatorAlways) {
scroll_indicator_position(self, k_scroll_indicator_horizontal);
}
}
The link is https://github.com/flexih/MazeScrollView

ScrollBar that functions just like the iOS built in one, but you can mess with the color and width.
-(void)persistantScrollBar
{
[persistantScrollBar removeFromSuperview];
[self.collectionView setNeedsLayout];
[self.collectionView layoutIfNeeded];
if (self.collectionView.contentSize.height > self.collectionView.frame.size.height + 10)
{
persistantScrollBar = [[UIView alloc] initWithFrame:(CGRectMake(self.view.frame.size.width - 10, self.collectionView.frame.origin.y, 5, (self.collectionView.frame.size.height /self.collectionView.contentSize.height) * self.collectionView.frame.size.height))];
persistantScrollBar.backgroundColor = [UIColor colorWithRed:207/255.f green:207/255.f blue:207/255.f alpha:0.5f];
persistantScrollBar.layer.cornerRadius = persistantScrollBar.frame.size.width/2;
persistantScrollBar.layer.zPosition = 0;
[self.view addSubview:persistantScrollBar];
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect rect = persistantScrollBar.frame;
rect.origin.y = scrollView.frame.origin.y + (scrollView.contentOffset.y *(self.collectionView.frame.size.height/self.collectionView.contentSize.height));
rect.size.height = (self.collectionView.frame.size.height /self.collectionView.contentSize.height) * self.collectionView.frame.size.height;
if ( scrollView.contentOffset.y <= 0 )
{
rect.origin.y = scrollView.frame.origin.y;
rect.size.height = rect.size.height + (scrollView.contentOffset.y);
}
else if (scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height)
{
rect.size.height = rect.size.height - ((scrollView.contentOffset.y + scrollView.frame.size.height) - scrollView.contentSize.height);
rect.origin.y = (self.collectionView.frame.origin.y + self.collectionView.frame.size.height - 5) - rect.size.height;
}
persistantScrollBar.frame = rect;
}

Swift 3
You can access the scrollbar using scrollView.subviews and modify the alpha as shown here. It works for me.
extension UIScrollView {
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for x in self.subviews {
x.alpha = 1.0
}
}
}
extension MyScrollViewDelegate : UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for x in scrollView.subviews {
x.alpha = 1.0
}
}
}

Looking through these answers, most of them are downright scary. Got this working in Swift 5 with the following. It still depends on the scroll view using subviews with class "_UIScrollViewScrollIndicator" - but at least there's no swizzling or app wide categories.
class IndicatorScrollView: UIScrollView {
weak var indicatorTimer: Timer?
override func didMoveToSuperview() {
super.didMoveToSuperview()
setupIndicatorTimer()
}
override func removeFromSuperview() {
super.removeFromSuperview()
indicatorTimer?.invalidate()
}
deinit {
indicatorTimer?.invalidate()
}
func setupIndicatorTimer() {
indicatorTimer?.invalidate()
indicatorTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(showIndicators), userInfo: nil, repeats: true)
}
#objc func showIndicators() {
subviews.forEach {
if String(describing: type(of: $0)).contains("ScrollIndicator") {
$0.alpha = 1
}
}
}
}

Related

Use UIPageViewController together with UIPanGestureRecognizer

My application is a page based application using UIPageViewController. Now I would like to able to move subviews vertically on my pages. After implementing a UIPanGestureRecognizer I was able to move the subviews but now my horizontal swiping to move through the pages does not work...
How can I combine those two gestures?
This is my panGestureRecognizer:
ContentViewController.swift
var panSwipe = UIPanGestureRecognizer(target: self, action: Selector("handlePanSwipe:"))
view.addGestureRecognizer(panSwipe)
...
func handlePanSwipe(sender:UIPanGestureRecognizer) {
var translation = sender.translationInView(self.view)
if sender.state == UIGestureRecognizerState.Began {
startLocation = sender.locationInView(self.view)
} else if (sender.state == UIGestureRecognizerState.Changed) {
self.settingsView.frame.origin.y = self.newSettingsFrameOrigin!.y + translation.y
} else if (sender.state == UIGestureRecognizerState.Ended) {
self.newSettingsFrameOrigin = self.settingsView.frame.origin
}
I want to be able to move the dark green subview up and down:
Thanks in advance!

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.

Can UITableView Section Headers be Changed when They are 'Floating'?

On iOS devices the section headers in UITableView's have a nice behavior where they stick or 'float' to the top of the screen as you scroll through a section. The section headers in my particular case are loaded from other XIB files.
Is it possible to change the section headers depending on whether or not they are currently floating? Specifically I'd like to add a small shadow to appear under the header only while it's stuck to the top of the view.
Thanks!
Here's the function I created to update whether each header has a shadow or not. All the section headers in this case are a UIView subclass ListHeader. They're retained and returned by the viewForHeaderInSection function.
- (void) updateHeaderShadows {
int i=0;
int sectionHeight = 0;
int totalHeight = 0;
UIView * sectionHeader;
while (i<[self numberOfSectionsInTableView:self.tableView]) {
sectionHeight = [self.tableView rectForSection:i].size.height;
sectionHeader = [self tableView:self.tableView viewForHeaderInSection:i];
if ([sectionHeader respondsToSelector:#selector(shadow)]) {
if (sectionHeader.frame.origin.y == totalHeight || sectionHeader.frame.origin.y == totalHeight + sectionHeight - sectionHeader.frame.size.height) {
[((ListHeader *) sectionHeader).shadow setHidden:YES];
} else {
[((ListHeader *) sectionHeader).shadow setHidden:NO];
}
}
totalHeight += sectionHeight;
i++;
}
}
I haven't tested it yet, but I don't see any reason why it wouldn't be possible.
Just make sure you set the right bounds (because your shadow needs to be on top of your view, not above it).
You can use the following approach:
Use scrollView:didScroll: to get notified about scroll-events.
In this method, check whether you need to add your shadow-view to your (floating) header-view.
If so, add it. (just [view addSubview:shadowView].) Something like CGRectMake(0.f, yourDefaultHeaderHeight, 320.f, yourShadowHeight) should be the frame of your shadowView.
Now, update the bounds of view, so it can show your shadowView: CGRectMake(0.f, 0.f - yourShadowHeight, 320.f, yourDefaultHeaderHeight + 2 * yourShadowHeight).
When you find out that your header isn't floating anymore (by using scrollView:didScroll:), remove the shadow-view.
Your headerViews bounds should be 0.f - yourShadowHeight because if you use just 0.f, it'll blur (I don't know why ...).
You would have to have your own UIView in the header. Then you would need a reference to it. Then hook into scrollViewWillBeginDragging: with your UIScrollViewDelegate. In that function, add the shadow to the custom view.
Hook into scrollViewDidEndDragging:willDecelerate: and remove the shadow in this function.
#Anthony Mattox answer for Swift
protocol SectionHeaderWithShadowProtocol where Self: UIView {
var shadow: Bool { get set }
}
class SectionHeaderView: UITableViewHeaderFooterView, SectionHeaderWithShadowProtocol {
#IBOutlet weak var shadowView: UIView!
var shadow: Bool = false {
didSet {
shadowView.isHidden = shadow
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateHeaderShadows()
}
func updateHeaderShadows() {
var i = 0
var sectionHeight: CGFloat = 0
var totalHeight: CGFloat = 0
while i < numberOfSections() {
sectionHeight = tableView.rect(forSection: i).size.height
if let sectionHeader = tableView.headerView(forSection: i) as? SectionHeaderWithShadowProtocol {
if sectionHeader.frame.origin.y == totalHeight || sectionHeader.frame.origin.y == totalHeight + sectionHeight - sectionHeader.frame.size.height {
sectionHeader.shadow = false
} else {
sectionHeader.shadow = true
}
}
totalHeight += sectionHeight
i += 1
}
}

UIScrollView custom paging size

paging in UIScrollView is a great feature, what I need here is to set the paging to a smaller distance, for example I want my UIScrollView to page less size that the UIScrollView frame width.
Thanks
There is a UIScrollView delegate method you can use. Set your class as the scroll view's delegate, and then implement the following:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGFloat kMaxIndex = 23;
CGFloat targetX = scrollView.contentOffset.x + velocity.x * 60.0;
CGFloat targetIndex = 0.0;
if (velocity.x > 0) {
targetIndex = ceil(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x == 0) {
targetIndex = round(targetX / (kCellWidth + kCellSpacing));
} else if (velocity.x < 0) {
targetIndex = floor(targetX / (kCellWidth + kCellSpacing));
}
if (targetIndex < 0)
targetIndex = 0;
if (targetIndex > kMaxIndex)
targetIndex = kMaxIndex;
targetContentOffset->x = targetIndex * (kCellWidth + kCellSpacing);
//scrollView.decelerationRate = UIScrollViewDecelerationRateFast;//uncomment this for faster paging
}
The velocity parameter is necessary to make sure the scrolling feels natural and doesn't end abruptly when a touch ends with your finger still moving. The cell width and cell spacing are the page width and spacing between pages in your view. In this case, I'm using a UICollectionView.
Change your scrollView size to the page size you want
Set your scroll.clipsToBounds = NO
Create a UIView subclass (e.g HackClipView) and override the hitTest:withEvent: method
-(UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event
{
UIView* child = [super hitTest:point withEvent:event];
if (child == self && self.subviews.count > 0)
{
return self.subviews[0];
}
return child;
}
Set the HackClipView.clipsToBounds = YES
Put your scrollView in this HackClipView (with the total scrolling size you want)
See this answer for more details
Update:
As stated in lucius answer you can now implement the UIScollViewDelegate protocol and use the - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset method. As the targetContentOffset is a pointer. Using this method will not guarantee you the same result with scroll view pages as the user can scroll through many pages at once. But setting the descelerationRate to fast will almost give you the same result
You should disable paging and add a UIPanGestureRecognizer to your scroll view and handle the paging yourself.
- (void)viewDidLoad {
[super viewDidLoad];
CGRect viewRect = self.view.bounds; // View controller's view bounds
theScrollView = [[UIScrollView alloc] initWithFrame:viewRect];
theScrollView.scrollsToTop = NO;
theScrollView.pagingEnabled = NO;
theScrollView.delaysContentTouches = NO;
theScrollView.delegate = self;
[self.view addSubview:theScrollView];
UIPanGestureRecognizer * peter = [[[UIPanGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePan:)]
autorelease];
[theScrollView addGestureRecognizer:peter];
}
-(void)handlePan:(UIPanGestureRecognizer*)recognizer{
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:{
// panStart and startPoint are instance vars for the viewContainer
panStart = theScrollView.contentOffset;
startPoint = [recognizer locationInView:theScrollView];
break;
}
case UIGestureRecognizerStateChanged:{
CGPoint newPoint = [recognizer locationInView:theScrollView];
CGFloat delta = startPoint.x - newPoint.x;
if ( abs(delta) > 2)
theScrollView.contentOffset = CGPointMake( theScrollView.contentOffset.x + delta, 0);
CGFloat moveDelta = panStart.x - theScrollView.contentOffset.x;
// current witdh should hold the currently displayed page/view in theScrollView
if ( abs(moveDelta) > (currentWidth * 0.40)){
panStart = theScrollView.contentOffset;
startPoint = newPoint;
//NSLog(#"delta is bigger");
if ( moveDelta < 0 )
[self incrementPageNumber]; // you should implement this method and present the next view
else
[self decrementPageNumber]; // you should implement this method and present the previous view
recognizer.enabled = NO; // disable further event until view change finish
}
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
recognizer.enabled = YES;
[self showDocumentPage:currentPage];
break;
default:
break;
}
}
Swift 4.1 solution that simplifies reusing:
/// Protocol that simplifies custom page size configuration for UIScrollView.
/// Sadly, can not be done better due to protocol extensions limitations - https://stackoverflow.com/questions/39487168/non-objc-method-does-not-satisfy-optional-requirement-of-objc-protocol
/// - note: Set `.decelerationRate` to `UIScrollViewDecelerationRateFast` for a fancy scrolling animation.
protocol ScrollViewCustomHorizontalPageSize: UIScrollViewDelegate {
/// Custom page size
var pageSize: CGFloat { get }
/// Helper method to get current page fraction
func getCurrentPage(scrollView: UIScrollView) -> CGFloat
/// Helper method to get targetContentOffset. Usage:
///
/// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
/// targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
/// }
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat
/// Must be implemented. See `getTargetContentOffset` for more info.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
}
extension ScrollViewCustomHorizontalPageSize {
func getCurrentPage(scrollView: UIScrollView) -> CGFloat {
return (scrollView.contentOffset.x + scrollView.contentInset.left) / pageSize
}
func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat {
let targetX: CGFloat = scrollView.contentOffset.x + velocity.x * 60.0
var targetIndex = (targetX + scrollView.contentInset.left) / pageSize
let maxOffsetX = scrollView.contentSize.width - scrollView.bounds.width + scrollView.contentInset.right
let maxIndex = (maxOffsetX + scrollView.contentInset.left) / pageSize
if velocity.x > 0 {
targetIndex = ceil(targetIndex)
} else if velocity.x < 0 {
targetIndex = floor(targetIndex)
} else {
let (maxFloorIndex, lastInterval) = modf(maxIndex)
if targetIndex > maxFloorIndex {
if targetIndex >= lastInterval / 2 + maxFloorIndex {
targetIndex = maxIndex
} else {
targetIndex = maxFloorIndex
}
} else {
targetIndex = round(targetIndex)
}
}
if targetIndex < 0 {
targetIndex = 0
}
var offsetX = targetIndex * pageSize - scrollView.contentInset.left
offsetX = min(offsetX, maxOffsetX)
return offsetX
}
}
Just conform to ScrollViewCustomPageSize protocol in your UIScrollView/UITableView/UICollectionView delegate and you are done, e.g.:
extension MyCollectionViewController: ScrollViewCustomPageSize {
var pageSize: CGFloat {
return 200
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
}
}
For a fancy scrolling I also recommend to set collectionView.decelerationRate = UIScrollViewDecelerationRateFast
Set the contentOffset in
-(void)scrollViewDidScroll:(UIScrollView *)scrollView method.
Also refer to UIScrollViewDelegate refernces
I had the same problem so I have made a custom UIScrollView.
It's available on Github now because when I searched I didn't find any solutions like this. Enjoy!
https://github.com/MartinMetselaar/MMCPSScrollView
MMCPSScrollView* scrollView = [[MMCPSScrollView alloc] initWithFrame:self.view.bounds];
[scrollView setType:MMCPSScrollVertical];
[scrollView setPageHeight:250];
[scrollView setPageSize:2];
[self.view addSubview:scrollView];
If you have any further questions about this component, just ask.
Adding gesture recognizers or other subviews and so on is silly. Just set the delegate for the scroll view an imlement on of the below :
// This is for a vertical scrolling scroll view.
// Let's say you want it to snap to every 160 pixels :
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
int y = scrollView.contentOffset.y;
int yOff = y % 160;
if(yOff < 80)
y -= yOff;
else
y += 160 - yOff;
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, y) animated:YES];
}
// This is for a horizontal scrolling scroll view.
// Let's say you want the same, to snap to every 160 pixels :
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
int x = scrollView.contentOffset.x;
int xOff = x % 160;
if(xOff < 80)
x -= xOff;
else
x += 160 - xOff;
[scrollView setContentOffset:CGPointMake(x, scrollView.contentOffset.y) animated:YES];
}
Swift 4.1, iOS11+:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = CGPoint(
x: round(targetContentOffset.pointee.x / pageWidth) * pageWidth,
y: targetContentOffset.pointee.y
)
}
I had the same problem short ago. My aproach was to add a second UIScrollView to the scrollview. So you can switch to the page. On that page it seems than if the page is bigger than the screen. I hope it works also in your situation. ;-)
Sandro Meier
This seemed to work a lot better for me:
UIScrollView Custom Paging
Here they are adding the scrollview (keeping it's paging niceness) as a subview to an ExtendedTouchView or subclass of UIVIew and overwriting the hit test method
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
if ([[self subviews] count] > 0) {
//force return of first child, if exists
return [[self subviews] objectAtIndex:0];
} else {
return self;
}
}
return nil;
}
This did exactly whatI wanted with minimal code and headache.
The easiest way is to add this code
scrollView.clipsToBounds = false
scrollView.removeGestureRecognizer(scrollView.panGestureRecognizer)
view.addGestureRecognizer(scrollView.panGestureRecognizer)