UIScrollView: single tap scrolls it to top - iphone

I have the UIScrollView with pagingEnabled set to YES, and programmatically scroll its content to bottom:
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
[scrollView setContentOffset:contentOffset animated:YES];
it scrolls successfully, but after that, on single tap its content scrolls up to offset that it has just before it scrolls down. That happens only when I programmaticaly scroll scrollView's content to bottom and then tap. When I scroll to any other offset and then tap, nothing is happened.
That's definitely not what I'd like to get. How that should be fixed?
Much thanks in advance!
Timur.

This small hack prevents the UIScrollView from scrolling when tapped. Looks like this is happening when the scroll view has paging enabled.
In your UIScrollView delegate add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
scrollView.pagingEnabled = self.scrollView.contentOffset.x < (self.scrollView.contentSize.width - self.scrollView.frame.size.width);
}
This disables the paging when the scroll view reaches the right end in horizontal scrolling (my use case, you can adapt it to other directions easily).

I just figured out what causes this problem, and how to avoid it. If you having pagingEnabled set to YES on a scroll view, you must set the contentOffset to be a multiple of the scroll view's visible size (i.e. you should be on a paging boundary).
Concrete example:
If your scroll view was (say) 460 pixels high with a content area of 920, you would need to set the content offset to EITHER 0 or 460 if you want to avoid the "scroll to beginning on tap" problem.
As a bonus, the end result will probably look better since your scroll view will be aligned with the paging boundaries. :)

The following workaround did help (assume that one extends UIScrollView with a category, so 'self' refers to its instance):
-(BOOL) scrolledToBottom
{
return (self.contentSize.height <= self.frame.size.height) ||
(self.contentOffset.y == self.contentSize.height - self.frame.size.height);
}
Then, one should sometimes turn pagingEnabled off, just at the position where scroll view reaches its bottom. In the delegate (pagingEnabled is initialy on of course, since the problem occurs only when it is enabled):
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.pagingEnabled == YES)
{
if ([scrollView scrolledToBottom] == YES)
scrollView.pagingEnabled = NO;
}
else
{
if ([scrollView scrolledToBottom] == NO)
scrollView.pagingEnabled = YES;
}
}

This seems to be a bug:
UIScrollView doesn't remember the position
I have tested this on iOS 4.2 (Simulator) and the issue remains.

When scrolling a ScrollView I would suggest using
[scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Where the rect is the position you're after. (In this case the rect would be the top of the scrollview).
Changing the content offset is not the correct way of scrolling a scrollview.

Related

UIScrollView: Animation banner effect

I have an UIScrollView with a PageControl, that show some pictures inside the view.
I'd like to create an animation, like a banner effect that changes the image after X seconds automatically.
Can do this with UIScrollView or should I use another way?
EDIT: #selector
- (void) changePage:(UIPageControl *)sender
{
if ([pageControl currentPage] < 3) {
[pageControl setCurrentPage:[pageControl currentPage] + 1];
} else{
[pageControl setCurrentPage:0];
}
}
UIScrollView is fine.
You have to set pagingEnabled on the scrollView and after a certain time, change the page selected on the scrollView.
Its simple, pagingEnabled changes current position as a page.
Change pageControl.pagingEnabled into pageControl.scrollEnabled
scrollEnabled : Scrolls like x/y axis as you scroll
pagingEnabled : Scrolls like current scroll view as a page for each scroll.
Now replace your pagingEnabled into scrollEnabled.

Scrolling, zooming UIScrollView and interface orientation rotation. How to use autoresize and more

This should be a pretty common thing to do, but I haven't been able to get it to work exactly right.
I have rectangular content. It normally fits in 320x361: portrait mode minus status bar minus ad minus tab bar.
I have put that content in a UIScrollView and enabled zooming. I also want interface rotation to work. The content will always be a tall rectangle, but when zoomed users might want to see more width at a time and less height.
What do I need to do in Interface Builder and code to get this done? How should I set my autoresizing on the different views? How do I set my contentSize and contentInsets?
I have tried a ton of different ways and nothing works exactly right. In various of my solutions, I've had problems with after some combination of zooming, interface rotation, and maybe scrolling, it's no longer possible to scroll to the entire content on the screen. Before you can see the edge of the content, the scroll view springs you back.
The way I'm doing it now is about 80% right. That is, out of 10 things it should do, it does 8 of them. The two things it does wrong are:
When zoomed in portrait mode, you can scroll past the edge of the content, and see a black background. That's not too much to complain about. At least you can see all the content. In landscape mode, zoomed or not, seeing the black background past the edge is normal, since the content doesn't have enough width to fill the screen at 1:1 zoom level (the minimum).
I am still getting content stuck off the edge when it runs on a test device running iOS 3.0, but it works on mine running 4.x. -- Actually that was with the previous solution. My tester hasn't tried the latest solution.
Here is the solution I'm currently using. To summarize, I have made the scroll view as wide and tall as it needs to be for either orientation, since I've found resizing it either manually or automatically adds complexity and is fragile.
View hierarchy:
view
scrollView
scrollableArea
content
ad
view is 320x411 and has all the autoresizing options on, so conforms to screen shape
scrollView is 480 x 361, starts at origin -80,0, and locks to top only and disables stretching
scrollableArea is 480 x 361 and locks to left and top. Since scrollView disables stretching, the autoresizing masks for its subviews don't matter, but I tell you anyway.
content is 320x361, starts at origin 80,0, and locks to top
I am setting scrollView.contentSize to 480x361.
shouldAutorotateToInterfaceOrientation supports all orientations except portrait upside down.
In didRotateFromInterfaceOrientation, I am setting a bottom content inset of 160 if the orientation is landscape, resetting to 0 if not. I am setting left and right indicator insets of 80 each if the orientation is portrait, resetting if not.
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
viewForZoomingInScrollView returns scrollableArea
// in IB it would be all options activated
scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
scrollView.contentSize = content.frame.size; // or bounds, try both
what do you mean with scrollableArea?
your minZoomScale is set to 1.0 thats fine for portrait mode but not for landscape. Because in landscape your height is smaller than in portrait you need to have a value smaller than 1.0. For me I use this implementation and call it every time, the frame of the scrollView did change:
- (void)setMaxMinZoomScalesForCurrentBounds {
CGSize boundsSize = self.bounds.size; // self is a UIScrollView here
CGSize contentSize = content.bounds.size;
CGFloat xScale = boundsSize.width / contentSize.width;
CGFloat yScale = boundsSize.height / contentSize.height;
CGFloat minScale = MIN(xScale, yScale);
if (self.zoomScale < minScale) {
[self setZoomScale:minScale animated:NO];
}
if (minScale<self.maximumZoomScale) self.minimumZoomScale = minScale;
//[self setZoomScale:minScale animated:YES];
}
- (void)setFrame:(CGRect)rect { // again, this class is a UIScrollView
[super setFrame:rect];
[self setMaxMinZoomScalesForCurrentBounds];
}
I don't think I understood the entire problem from your post, but here's an answer for what I did understand.
As far as I know (and worked with UIScrollView), the content inside a UIScrollView is not automatically autoresized along with the UIScrollView.
Consider the UIScrollView as a window/portal to another universe where your content is. When autoresizing the UIScrollView, you are only changing the shape/size of the viewing window... not the size of the content in the other universe.
However, if needed you can intercept the rotation event and manually change your content too (with animation so that it looks good).
For a correct autoresize, you should change the contentSize for the scrollView (so that it knows the size of your universe) but also change the size of UIView. I think this is why you were able to scroll and get that black content. Maybe you just updated the contentSize, but now the actuall content views.
Personally, I haven't encountered any case that required to resize the content along with the UIScrollView, but I hope this will get you started in the right direction.
If I understand correctly is that you want a scrollview with an image on it. It needs to be fullscreen to start with and you need to be able to zoom in. On top of that you want it to be able to rotate according to orientation.
Well I've been prototyping with this in the past and if all of the above is correct the following code should work for you.
I left a bit of a white area for the bars/custombars.
- (void)viewDidLoad
{
[super viewDidLoad];
//first inits and allocs
scrollView2 = [[UIScrollView alloc] initWithFrame:self.view.frame];
imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"someImageName"]];
[scrollView2 addSubview:imageView];
[self drawContent]; //refreshing the content
[self.view addSubview:scrollView2];
}
-(void)drawContent
{
//this refreshes the screen to the right sizes and zoomscales.
[scrollView2 setBackgroundColor:[UIColor blackColor]];
[scrollView2 setCanCancelContentTouches:NO];
scrollView2.clipsToBounds = YES;
[scrollView2 setDelegate:self];
scrollView2.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[scrollView2 setContentSize:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)];
[scrollView2 setScrollEnabled:YES];
float minZoomScale;
float zoomHeight = imageView.frame.size.height / scrollView2.frame.size.height;
float zoomWidth = imageView.frame.size.width / scrollView2.frame.size.width;
if(zoomWidth > zoomHeight)
{
minZoomScale = 1.0 / zoomWidth;
}
else
{
minZoomScale = 1.0 / zoomHeight;
}
[scrollView2 setMinimumZoomScale:minZoomScale];
[scrollView2 setMaximumZoomScale:7.5];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
if (interfaceOrientation == UIInterfaceOrientationPortrait || interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
// Portrait
//the 88pxls is the white area that is left for the navbar etc.
self.scrollView2.frame = CGRectMake(0, 88, [UIScreen mainScreen].bounds.size.width, self.view.frame.size.height - 88);
[self drawContent];
}
else {
// Landscape
//the 88pxls is the white area that is left for the navbar etc.
self.scrollView2.frame = CGRectMake(0, 88, [UIScreen mainScreen].bounds.size.height, self.view.frame.size.width);
[self drawContent];
}
return YES;
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
I hope this will fix your troubles. If not leave a comment.
When you want to put a content (a UIView instance, let's call it theViewInstance ) in a UIScrollView and then scroll / zoom on theViewInstance , the way to do it is :
theViewInstance should be added as the subview of the UIScrollView
set a delegate to the UIScrollView instance and implement the selector to return the view that should be used for zooming / scrolling:
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return theViewInstance;
}
Set the contentSize of the UIScrollView to the frame of the theViewInstance by default:
scrollView.contentSize=theViewInstance.frame.size;
(Additionally, the accepted zoom levels can be set in the UIScrollView :)
scrollView.minimumZoomScale=1.0;
scrollView.maximumZoomScale=3.0;
This is the way a pinch to zoom is achieved on a UIImage : a UIImageView is added to a UIScrollView and in the UIScrollViewDelegate implementation, the UIImageView is returned (as described here for instance).
For the rotation support, this is done in the UIViewController whose UIView contains the UIScrollView we just talked about.

Can I override scroll view's automatic behavior to scroll to the first responder?

I have a UITextField inside a UIScrollView (a few levels deep). I am watching UIKeyboardDidShowNotification, and also calling the same code when I manually change the first responder (I might change to a different text field without momentarily hiding the keyboard). In that code I use scrollRectToVisible:animated: to make sure the UITextField is visible.
I was having a huge headache debugging why that was acting funny, but I realized now that UIScrollView automatically ensures that the first responder is within its bounds. I am changing the frame of the UIScrollView so that none of it is hidden behind the keyboard.
However, my code can be slightly smarter than their code, because I want to show not only the UITextField, but some nearby related views as well. I try to show those views if they will fit; if not whatever, I try to show as much of them as I can but at least ensure that the UITextField is visible. So I want to keep my custom code.
The automatic behavior interferes with my code. What I see is the scroll view gently scroll up so that the bottom edge of my content is visible, then it snaps down to where my code told it to position.
Is there anyway to stop the UIScrollView from doing its default capability of scrolling the first responder into view?
More Info
On reviewing the documentation I read that they advise to change the scroll view's contentInset instead of frame. I changed that and eliminated some unpredictable behavior, but it didn't fix this particular problem.
I don't think posting all the code would necessarily be that useful. But here is the critical call and the values of important properties at that time. I will just write 4-tuples for CGRects; I mean (x, y, width, height).
[scrollView scrollRectToVisible:(116.2, 71.2, 60, 243) animated:YES];
scrollView.bounds == (0, 12, 320, 361)
scrollView.contentInset == UIEdgeInsetsMake(0, 0, 118, 0)
textField.frame == (112.2, 222.6, 24, 24)
converted to coordinates of the immediate subview of scrollView == (134.2, 244.6, 24, 24)
converted to coordinates of scrollView == (134.2, 244.6, 24, 24)
So the scroll view bottom edge is really at y == 243 because of the inset.
The requested rectangle extends to y == 314.2.
The text field extends to y == 268.6.
Both are out of bounds. scrollRectToVisible is trying to fix one of those problems. The standard UIScrollView / UITextField behavior is trying to fix the other. They don't come up with quite the same solution.
I didn't test this particular situation, but I've managed to prevent a scrollview from bouncing at the top and bottom by subclassing the scrollview and overriding setContentOffset: and setContentOffset:animated:. The scrollview calls this at every scroll movement, so I'm fairly certain they will be called when scrolling to the textfield.
You can use the delegate method textFieldDidBeginEditing: to determine when the scroll is allowed.
In code:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.blockingTextViewScroll = YES;
}
-(void)setContentOffset:(CGPoint)contentOffset
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset];
}
}
-(void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset animated:animated];
}
}
If your current scroll behaviour works with a setContentOffset: override, just place it inside the else blocks (or preferably, in a method you call from the else blocks).
In my project I have succeeded to achieve this by performing my scroll only after some delay.
- (void)keyboardWillShow:(NSNotification *)note
{
NSDictionary *userInfo = note.userInfo;
CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets contentInsets = self.tableView.contentInset;
contentInsets.bottom += keyboardFrame.size.height;
[self.tableView setContentInset:contentInsets];
[self performSelector:#selector(scrollToEditableCell) withObject:nil afterDelay:0];
}
Also there is other possibility to make your view with additional views to be first responder and fool scroll view where to scroll. Haven't tested this yet.
This may turn out to be useless, but have you tried setting scrollView.userInteractionEnabled to NO before calling scrollrectToVisible: & then setting it back to YES? It may prevent the automatic scrolling behavior.
Try changing the view autoresizing to UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin. The default is FlexibleTopMargin so maybe thats the reason. btw scrollRectToVisible: is using the scrollView.contentSize.
The other thing you can try to change the scrollView size first and then apply the scrollRectToVisible: change. First frame change, then content change. (Maybe observe the keyboard did appear event)
The automatic scrolling behavior seems to be especially buggy starting in iOS 14. I alleviated the problem by subclassing UIScrollView and overriding setContentOffset to do nothing. Here is the bases of my code.
class ManualScrollView: UIScrollView {
/// Use this function to set the content offset. This will forward the call to
/// super.setContentOffset(:animated:)
/// - Parameters:
/// - contentOffset: A point (expressed in points) that is offset from the content view’s origin.
/// - animated: true to animate the transition at a constant velocity to the new offset, false to make the transition immediate.
func forceContentOffset(_ contentOffset: CGPoint, animated: Bool) {
super.setContentOffset(contentOffset, animated: animated)
}
/// This function has be overriden to do nothing to block system calls from changing the
/// content offset at undesireable times.
///
/// Instead call forceContentOffset(:animated:)
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
}
}
This works but you have to deal with reimplementing many of the scroll views behaviors and methods that you normally get for free. Since scrollRectToView and scrollToView both use setContentOffset you also have to reimplement these if you want them to work.

Limiting the scrollable area in UIScrollView

I have a UIScrollView that is scrolling a fairly large UIView.
At certain times I want to limit the area the user can scroll around in. For example, I may only want to allow them to view the bottom quarter of the view.
I am able to limit the area by overriding scrollViewDidScroll and then calling setContentOffset if the view has scrolled too far. But this way I can't get it bounce back as smoothly as the UIScrollView can naturally do when scrolling beyond the bounds of the UIView.
Is there a better way to limit the scrollable area in a UIScrollView?
I would change the contentSize property of the scroll view to the size of the area you want the user to be able to scroll around in and adjust the frame.origin of the subview such the upper left boundary you want appears at (0, 0) relative to the scroll view. For example, if your view is 800 points tall and you want to show the bottom quarter, set the height of contentSize to 200 and set the y component of view.frame.origin to -600.
I've found something that works for me. It let's you scroll to point 0,0 but no further:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x <= -1) {
[scrollView setScrollEnabled:NO];
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
[scrollView setScrollEnabled:YES];
}
}
You could do the same for top, bottom or right (x or y)
a small improvement on Yoko's answer in Swift 4 will be
override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > 600 {
let anim = UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) {
scrollView.isScrollEnabled = false
scrollView.setContentOffset(CGPoint(x: 0, y: 600), animated: false)
scrollView.isScrollEnabled = true
}
anim.startAnimation()
}
}
which will make the scrollview animate really similar to what its supposed to do. The slower drag when you are in the "bounce" area will not work and animation duration has to depend on the distance (not constant like here) if you want to be exact. You can also try to do this logic in scrollViewDidScroll and see how it differs. The key thing is that setContentOffset(_:,animated:) has to be with animated: false so that the UIViewPropertyAnimator's block can capture it and animate it
Another approach is to override the UIScrollView's method:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event.
Returning YES will allow the user to scroll. Returning NO will not.
NOTE: This will disable all touches to any views imbedded inside the UIScrollView that pointInside returns NO to. Useful if the area you don't want to scroll from doesn't have any interaction.
This example only allows the UIScrollView to scroll when the user is scrolling over a UITableView. (A UITableView and two UIViews are imbedded inside the UIScrollView)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event] && ![subview isKindOfClass:[UITableView class]]) {
return NO;
}
}
return YES;
}

How do I stop a UIScrollView from bouncing horizontally?

I have a UIScrollView that shows vertical data, but where the horizontal component is no wider than the screen of the iPhone. The problem is that the user is still able to drag horizontally, and basically expose blank sections of the UI. I have tried setting:
scrollView.alwaysBounceHorizontal = NO;
scrollView.directionalLockEnabled = YES;
Which helps a little, but still doesn't stop the user from being able to drag horizontally. Surely there is a way to fix this easily?
scrollView.bounces = NO;
Worked for me.
That's strange, because whenever I create a scroll view with frame and content size within the bounds of the screen on either dimension, the scroll view does not scroll (or bounce) in that direction.
// Should scroll vertically but not horizontally
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
scrollView.contentSize = CGSizeMake(320, 1000);
Are you sure the frame fits completely within the screen and contentSize's width is not greater than the scroll view's width?
The checkbox for bounce vertically in storyboard-scrollview can simply help...
That works for me in Swift:
scrollView.alwaysBounceHorizontal = false
scrollView.bounces = false
Try setting scrollView.bounces to NO and scrollView.alwaysBounceVertical to YES.
Whether or not a view scrolls (and bounces) horizontally depends on three things:
The content size
The left and right content insets
The width of the scroll view -
If the scroll view can fit the content size plus the insets then it doesn't scroll or bounce.
Avoid horizontal bouncing like so:
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width - scrollView.contentInset.left - scrollView.contentInset.right, height);
I am adding this answer because the previous ones did not take contentInset into account.
Make sure the UIScrollView's contentSize is not wider than the UIScrollView itself. In my own apps this was enough to avoid horizontal scrolling, except in cases where I got really crazy swiping in random directions (e.g., starting a scroll while the view was still decelerating).
If anyone developing for OS X is looking here, as of OS X 10.7, the solution is to set the horizontalScrollElasticity property to false/NO on the scroll view, like this:
Swift:
scrollView.horizontalScrollElasticity = false
Objective-C:
scrollView.horizontalScrollElasticity = NO
Something to keep in mind: You know there's nothing extra to see horizontally, but will your users know that? You may want a little horizontal bounce, even if there's no extra content to show horizontally. This let's the user know that their attempts to scroll horizontally are not being ignored, there's just nothing there for them to see. But, yeah, often you really don't want the bounce.
My version for webViews, a common solution:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[webView.scrollView setContentSize: CGSizeMake(webView.frame.size.width, webView.scrollView.contentSize.height)];
}
You can disable horizontal bounces like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.x < 0) {
scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y);
} else if (scrollView.contentOffset.x > scrollView.contentSize.width) {
scrollView.contentOffset = CGPointMake(scrollView.contentSize.width, scrollView.contentOffset.y);
}
}
It resets the contentOffset.x manually and won't affect the vertical bounces. It works...
In my case, i just need to set this line:
collectionView.bounces = false