I want to use the scrollview as something like a picker in horizontal mode.
The scrollview holds up to seven subviews.
Each subview represents a value.
Always three views are visible and the one in the middle is the selected one.
Scrollview visible at start:
__ | V1 | V2
Scrollview set to view/value two:
V1 | V2 | V3
Scrollview set to last value:
V2 | V3 | __
The real problem I have got is the "pagingEnabled" flag.
If pagingEnabled is set to YES the scrollview pages always three subviews/values instead of only one.
If pagingEnabled is set to NO the scrollview does not clinch.
Is there a nice solution for my problem?
Thanks a lot,
Dan ;)
Change the frame of the scrollview to be as if it were only displaying the middle view (i.e. a third of its original width, and offset by the same amount), but then set its clipsToBounds property to NO.
I found a solution if anyone else is interested.
Assign you view the delegate of a scrollview. Ovveride scrollViewDidEndDecelerating, afterwards get your current index(page you want) by doing something like.
NSNumber* currentIndex = [NSNumber numberWithInt:round(scrollview.Contentoffset.x / PAGE_SIZE)];
//Then just update your scrollviews offset with
[scrollview setContentOffset:CGPointMake([currentIndex intValue] * PAGE_SIZE, 0) animated:YES];
Since iOS 5, there's the scrollViewWillEndDragging:withVelocity:targetContentOffset: delegate method on UIScrollViewDelegate. This allows you to implement arbitrary paging.
For this to work, you first need to set the pagingEnabled property to NO, otherwise the delegate method I'm talking about isn't called. The scroll view now calls this delegate method whenever the user lifts his finger and the scroll view wants to determine where to finish the scrolling.
The magic is the last argument, targetContentOffset: it's a pointer to a CGPoint and used as a in/out variable. This means this variable tells you where the scrollview wants to scroll to. But it allows you modify this target location. The velocity might also be of interest, it can give you an indication whether the user "pushed" the scroll view or moved it, stopped, then lifted his finger.
For example, here's an implementation that rounds the target x location to the nearest multiple of 100, thus making "pages" of 100 points width.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
targetContentOffset->x = round(targetContentOffset->x / 100.0) * 100.0;
}
Related
I've set up an infinite scroll view and when it reaches 0 content offset I set it to max content offset and vice versa.
i.e.
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
This works but it stops the UIScrollView decelerating.
Is there a way to do this but keep the UIScrollView decelerating?
I tried this...
float declerationRate = scrollView.decelerationRate;
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width, 0) animated:NO];
scrollView.decelerationRate = declerationRate;
but it didn't work.
EDIT
I just realised that decelerationRate is the setting to determine how slow/fast the deceleration is.
What I need is to get the current velocity from the scrollView.
Right I had to tweak the idea a bit.
Turns out trying to set the velocity of a UIScrollView is difficult... very difficult.
So anyway, I kind of tweaked it.
This is actually a mini project after answering someone else's SO question and thought I'd try to solve it myself.
I want to create a spinner app that I can swipe to spin an arrow around so it spins and decelerates to a point.
What I did was set up a UIImageView with the arrow pointing up.
Then covering the UIImageView is a UIScrollView.
Then in the code...
#interface MyViewController () <UIScrollViewDelegate>
#property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
#property (nonatomic, weak) IBOutlet UIImageView *arrowView;
#end
#implementation MyViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//make the content size really big so that the targetOffset of the deceleration will never be met.
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * 100, self.scrollView.frame.size.height);
//set the contentOffset of the scroll view to a point in the center of the contentSize.
[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width * 50, 0) animated:NO];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)rotateImageView
{
//Calculate the percentage of one "frame" that is the current offset.
// each percentage of a "frame" equates to a percentage of 2 PI Rads to rotate
float minOffset = self.scrollView.frame.size.width * 50;
float maxOffset = self.scrollView.frame.size.width * 51;
float offsetDiff = maxOffset - minOffset;
float currentOffset = self.scrollView.contentOffset.x - minOffset;
float percentage = currentOffset / offsetDiff;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI * 2 * percentage);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//the scrollView moved so update the rotation of the image
[self rotateImageView];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//the scrollview stopped moving.
//set the content offset back to be in the middle
//but make sure to keep the percentage (used above) the same
//this ensures the arrow is pointing in the same direction as it ended decelerating
float diffOffset = scrollView.contentOffset.x;
while (diffOffset >= scrollView.frame.size.width) {
diffOffset -= scrollView.frame.size.width;
}
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 50 + diffOffset, 0) animated:NO];
}
#end
This gives the desired effect of a spinner like on Wheel of Fortune that will spin endlessly in either direction.
There is a flaw though. If the user keeps spinning and spinning without letting it stop it will only go 50 spins in either direction before coming to a stop.
As I said in the comment, the way you're doing is producing an expected result, one way to do what you want is to set the content offset to the top but then by using the content size height and deceleration value, you could animate the content offset again, check out this answer: https://stackoverflow.com/a/6086521/662605
You will have to play around with some math before it feel right but I think this is a reasonable workaround.
The lower the deceleration, the longer the animation (time) and the more it will animate (distance). Let me know what you think.
As you've said, the deceleration is probably not the only thing you need. So you could try KVO on the contentOffset to calculate the mean velocity over half a second perhaps to get an idea of speed.
There are probably a few different ways to do this. Ideally, you won't have to do any type of calculation to simulate the remaining deceleration physics or mess around at all with UIScrollView's internals. It's error-prone, and it's not likely to perfectly match the UIScrollView physics everyone's used to.
Instead, for cases where your scroll view is merely a driver for a gesture (i.e., you don't actually need to display anything in it), I think it's best to just have a massively wide scroll view. Set its initial content offset to the center of its content size. In scrollViewDidScroll(_:), calculate a percentage of the traversed screen width:
let pageWidth = scrollView.frame.width
let percentage = (scrollView.contentOffset.x % pageWidth) / pageWidth
This will basically loop from 0.0 to 1.0 (moving right) or from 1.0 to 0.0 (moving left) over and over. You can forward those normalized values to some other function that can respond to it, perhaps to drive an animation. Just structure whatever code responds to this such that it appears seamless when jumping from 0.0 to 1.0 or from 1.0 to 0.0.
Of course, if you need whatever you're looping to occur faster or slower than the normal scroll view speed, just use a smaller or larger fraction of the screen width. I just picked that arbitrarily.
If you're worried about hitting the edges of the scrollable content, then when the scroll view comes to a complete rest1, reset its content offset to the same initial center value, plus whatever remainder of the screen width (or whatever you're using) the scroll view was at when it stopped scrolling.
Given the resetting approach above, even for scroll views where you're hosting visible content, you can effectively achieve an infinitely scrolling view as long as you update whatever view frames/model values to take into account the reset scroll offset.
1 To properly capture this, implement scrollViewDidEndDecelerating(_:) and scrollViewDidEndDragging(_:willDecelerate:), only calling your "complete rest" function in the latter case when willDecelerate is false. Always call it in the former case.
I found that if you call
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
at point(0,0), it will trigger bounces action for UIScrollView, then the deceleration will stop. The same situation takes place when you call setContentOffset to bound of the content.
So you can call setContentOffset to point(10,10) or somewhere else to keep the deceleration easily.
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.
so lets say I have a UIScrollView, within it are 3 UIViews, within those there is a UISlider in each one. they are positioned vertically in the UIScrollView.
I now have a 4th UIView also in the UIScrollView which I wish to move around depending on the position of the slider which has been used.
so within my sliderChanged method which i pass the sender, i get the position of the slider, and adjust the position of the 4th UIWindow to its y. This works great on the first UIView, but once on another UIView which has forced me to scroll down, using the slider moves the 4th UIView but stays at the beginning of the UIScrollView
I am using:
[4thView setCenter:CGPointMake([4thView center].x, [slider center].y+10)];
what I need is to get the position of the slider relative to the content of the scrollView and not relative to its UIView, so that I may set the 4th view again relative to the scrollView content.
You can convert the points by UIView's instance methods.
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view
For example, I want to convert a ponint on the viewA to the scrollViewB's coordinates.
CGPoint aPtInScrollView = [viewA convertPoint:aPoint toView:scrollViewB];
Or, I want to know the position of viewA in scrollViewB.
CGPoint aPosViewA = [scrollViewB convertPoint:CGPointZero fromView:viewA];
Using the previous answer I took it a bit further to solve the issue I was having.
If you have multiple UITextView's within multiple UIView's all inside a single UIScrollView: this will animate and scroll to them.
This could be also be applied to UITextField's as well.
-(void)textViewDidBeginEditing:(UITextView *)textView {
[self.theScrollView setContentOffset:[self.theScrollView convertPoint:CGPointMake(textView.frame.origin.x, textView.frame.origin.y) fromView:textView.superview] animated:YES];
}
If you have labels above your textviews, just offset the x or y by that amount.
Setup: I have a UITableView, each UITableViewCell has a UIScrollView. What I am trying to do is to get all of the UIScrollViews to scroll together, such that when you scroll one of them all of the UIScrollViews appear to scroll simultaneously.
What I've done is subclass UITableView so that it has an array of all of the UIScrollViews within its table cells. I then forwarded TouchesBegan, TouchesMoved, TouchesCancelled, and TouchesEnded from the UITableView to all of the UIScrollViews in the array.
This doesn't appear to work. The UIScrollViews do not scroll! The only way I've managed to get this to work is to call the setContentOffset: method on the scrollviews. However, this is a pretty bad solution since it doesn't give you the swiping and deceleration features of the UIScrollView.
Any ideas on why my touches methods aren't getting to the UIScrollViews? Or a better way to implement this?
Ok, got it working. Thanks for the tips Ricki!
2 things to add to Ricki's solution, if you want to avoid an infinite loop, you have to check to see whether the scrollView's tracking or dragged properties are set. This will insure that only the ScrollView that is actually being dragged is calling the delegate.
- (void)scrollViewDidScroll:(UIScrollView *) theScrollView {
if (theScrollView.dragging || theScrollView.tracking)
[self.delegate scrolling:[theScrollView contentOffSet]];
}
Also, in the scrolling method of the delegate, I set animated to NO, this got rid of the delay between the initial swipe and the other scrollviews getting updated.
I did something "similar" where I had 4 scrollViews incased inside a parent view.
I placed a scrollView inside a UIView, this UIView was passed a delegate from its parentView, that was the view who kept track of all the scrollViews. The UIView containing a scrollVIew implemented the UIScrollViewDelegate and this method;
- (void)scrollViewDidScroll:(UIScrollView *) theScrollView {
[self.delegate scrolling:[self.scrollView contentOffSet]];
}
Now the parent view did this on all the scrollViews:
- (void) scrolling:(CGFloat) offset {
for(UIScrollView *s in self) {
[s setContentOffset:offset animated:YES];
}
}
It is of course a bit of a strain on the CPU, but scrolling several views will be that under any circumstances :/
Hope this was something in the direction of what you needed, and that it made any sense.
Added:
I took me 8 different paths and a lot of mass chaos before I made it work. I dropped the touchedBegan approach early, there is just no way to write something that comes close to Apples swipe, flick, scroll algorithms.
I don't know if the tableview and scrollview will "steal" each others touch events, but as I can read from your description you made that part work.
A follow up idea to ease the CPU usage. add each scrollview to a cell, set its tag=14, now when scrolling asked for all visible cells only, ask for viewWithTag=14, set the contentOffset on this. Save the content offset globally so you can assign it to cells being scrolled onto the screen in cellForRowAtIndexPath.
So set the offSet to a global property, in cellForRowAtIndexPath find the view with tag = 14, set its offset. This way you don't even need a reference to the scrollViews only the delegate.
If you have differently sized UIScrollViews and are using paging, this works great:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)_scrollView {
#pragma unused(_scrollView)
categoryPageControlIsChangingPage = NO;
for (UIImageView *iv in [categoryScrollView subviews]) {
iv.alpha = (iv.tag != categoryPageControl.currentPage+1)?0.5f:1.0f;
ILogPlus(#"%i %i", iv.tag, categoryPageControl.currentPage+1);
}
[self scrolling:_scrollView];
}
- (void)scrolling:(UIScrollView *)sv {
CGFloat offsetX = sv.contentOffset.x;
CGFloat ratio = offsetX/sv.contentSize.width;
if ([sv isEqual:categoryScrollView]) {
[categoryScrollViewLarge setContentOffset:CGPointMake(ratio*categoryScrollViewLarge.contentSize.width, 0) animated:YES];
}else {
[categoryScrollView setContentOffset:CGPointMake(ratio*categoryScrollView.contentSize.width, 0) animated:YES];
}
}
Is there a way to deactivate the decelerating of a UIScrollView?
I want to allow the user to scroll the canvas, but I don't want that the canvas continues scrolling after the user lifted the finger.
This can be done by utilizing the UIScrollView delegate method scrollViewWillBeginDecelerating to automatically set the content offset to the current screen position.
To implement:
Assign a delegate to your UIScrollView object if you have not already done so.
In your delegate's .m implementation file, add the following lines of code:
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
[scrollView setContentOffset:scrollView.contentOffset animated:YES];
}
Voila! No more auto-scroll.
For iOS 5.0 or later, there is a better method than calling setContentOffset:animated:.
Implement delegate method scrollViewWillEndDragging:withVelocity:targetContentOffset: in your .m file:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
targetContentOffset.pointee = scrollView.contentOffset;
}
Assigning the current offset to targetContentOffset stops the UIScrollView from auto-scrolling.
You can just turn up the deceleration rate very high. With an infinite rate, it would stop immediately. Try setting the rate to these constants:
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
and
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
If fast still isn't fast enough for you, UIScrollViewDecelerationRateFast is just typedef'ed as a float, so you can just multiply it by a factor of 10 or so to speed it up even more.
Just set the decelerationRate property to 0
It will disable the auto scrolling property. But keep in mind the user interaction will become bad if scrollview contentsize is big.
Previous Swift version:↓
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
Current Swift 4.2 version code:↓
scrollView.decelerationRate = UIScrollViewDecelerationRateFast