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];
}
}
Related
I am having an horizontal scrollview in an UIViewController, where i have many images in small sizes. I am keeping the images in scrollview because the images are more, so user can scroll horizontally and choose images. But, the problem is, i have to select an image and drag and drop to that UIViewController view. But, since the images are in scrollview, drag and drop images into UIViewcontroller's view is not working, not detecting the touch events too.
Please NOTE: If i don't have scrollview but just keeping the images also into UIViewcontroller's view itself, drag and drop the images on the same screen, is working very well.
How can I resolve this when I need to have scrollview and drag and drop images, any advice/help please?
Hi Getsy,
I am not going to provide you code directly but give idea how to manage this.
You can manage this way, When you get touch on your object in scrollView at that time or when you move that object by draging at that time disable scroll by myScroll.scrollEnabled = NO;
Then When on endTouch you can enable Scroll by myScroll.scrollEnabled = YES; So by this you can manage you object moving in scroll hope you got logic.
Here is the demo code : Drag and Drop with ScrollView. which has same logic of Disabling scroll view on touchesMoved: and Enabling scroll view on touchesEnded:.
I did implement that behaviour before without any subclassing.
I used canCancelContentTouches = NO of the UIScrollView to make sure the subviews handle there touches on their own. If a subview (in your case an image) was touched, i moved the view out of the scrollview onto the superview and started tracking it's dragging. (You have to calculate the correct coordinates within the new superview, so it stays in place).
After dragging finishes, i checked if the target area was reached, otherwise I moved it back into the scrollview. If that's not detailed enough I could post some code.
Well here is my example code: Github: JDDroppableView
Getsy,
Try the code for drag and drop the objects :
-(void)dragAndDropWithGesture {
UILongPressGestureRecognizer *downwardGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(dragGestureChanged:)];
[scrollViewAlfabeto addGestureRecognizer:downwardGesture];
for (UIGestureRecognizer *gestureRecognizer in myscrollView.gestureRecognizers)
{
[gestureRecognizer requireGestureRecognizerToFail:downwardGesture];
}
}
- (void) dragGestureChanged:(UIPanGestureRecognizer*)gesture
{
CGPoint point = [gesture locationInView:scrollViewAlfabeto];
if (gesture.state == UIGestureRecognizerStateBegan)
{
[imageViewToMove removeFromSuperview];
[self.view addSubview:imageViewToMove];
UIView *draggedView = [myscrollView hitTest:point withEvent:nil];
if ([draggedView isKindOfClass:[UIImageView class]])
{
imageViewToMove = (UIImageView*)draggedView;
}
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
imageToMove.center = point;
}
else if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed)
{
// Determine if dragged view is in an OK drop zone
// If so, then do the drop action, if not, return it to original location
NSLog(#"point.x final:%f", point.x);
NSLog(#"point.y final:%f", point.y);
if (CGRectContainsPoint(goal.frame, point)){
imageToMove.frame = CGRectMake(167, 159, 100, 100);
}
else{
[imageToMove removeFromSuperview];
[myscrollView addSubview:imageToMove];
[imageToMove setFrame:CGRectMake(12, 38, 100, 100)];
imageToMove = nil;
}
}
}
May this code will help you out.
For a similar problem, I made UIScrollView subclass like the following: PoliteScrollView passes touch messages to it's subviews when it determines that they are being dragged.
#interface PoliteScrollView : UIScrollView
// number of pixels perpendicular to the scroll views orientation to make a drag start counting as a subview drag
// defaults to 1/10 of the scroll view's width or height, whichever is smaller
#property(nonatomic,assign) CGFloat subviewDraggingThreshold;
// yes when a subview is being dragged
#property(nonatomic,assign) BOOL isDraggingSubview;
// the subview being dragged
#property(nonatomic,strong) UIView *draggingSubview;
#end
#protocol PoliteScrollViewDelegate <NSObject>
#optional
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesBegan:(NSSet *)touches;
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesMoved:(NSSet *)touches;
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesEnded:(NSSet *)touches;
#end
The key design idea is that dragging in a scroll view is ambiguous. Is the user scrolling or dragging a subview? PoliteScroll view handles this by providing two things: (1) a notion of orientation (horizontal if it's longer than it is wide, vertical otherwise), and (2) a threshold distance for what constitutes a drag in the direction perpendicular to it's orientation. (defaults to 1/10 the width or height).
I pasted this and several other files are in a paste bin, containing the following:
PoliteScrollView .h and .m
DraggableImageView.h and .m - that changes it's position when it gets touch messages.
ViewController.m - that demonstrates thetwo in combination.
To combine these in a project, paste the paste bin into files appropriately named, add a storyboard with a PoliteScrollView (be sure to set it's delegate), add in some images (the ViewController tries to add puppy0.jpeg to puppy4.jpeg.
I created an example which illustrates how to drag and drop between two or more views:
http://www.ancientprogramming.com/2012/04/05/drag-and-drop-between-multiple-uiviews-in-ios/
I found it a good idea to register the gesture recognizer to a different view than the actual views being dragged. This will make sure the gestures continue even though the dragged view changes its 'parent' view.
Maybe it can give some inspiration
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.
I am using a UIWebView to show a PDF. I need to track some UIScrollView events, so I set the built-in UIScrollView like this:
for(UIScrollView *s in webView.subviews) {
s.delegate = self;
}
But the problem is that when I do this, I lose the ability to pinch to zoom (like I could before because the UIWebView's Scale Pages to Fit property was set). How come this happens? What can I do to fix it? I went ahead and implemented the UIScrollView shouldZoom method, but when I pass back the scrollView as the view to zoom on, it zooms from the corner, not where my fingers are.
Does anybody know a solution to this problem? How can I have the UIScrollView delegate set, and still retain natural zooming ability?
Thanks!
You are messing with some UIWebView internals. This feels like a bad hack, but I think you could forward all the UIScrollViewDelegate methods back to UIWebView in addition to doing whatever you need to do.
Also, UIWebView has many subviews. You should check and override the delegate just for the UIScrollView.
for (UIView *subview in webViews.subviews) {
if ([subview isKindOfClass:[UIScrollView class]]) {
subview.delegate = self;
}
}
Either handle the following events in your delegate:
– viewForZoomingInScrollView:
– scrollViewWillBeginZooming:withView:
– scrollViewDidEndZooming:withView:atScale:
– scrollViewDidZoom:
or save the original delegate
origDelegate = s.delegate;
s.delegate = self;
and forward the calls, e.g:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return [origDelegate viewForZoomingInScrollView:scrollView];
}
I'm using UIScrollView's canceling touch ability with canCancelContentTouches.
However, I 'd like the uiscrollview to attempt to cancel touch when it detected horizontal dragging(not vertical).
(Hope solution would be available under < iOS 3.13)
Thank you
Implement the UIScrollViewDelegate and then use something like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[scrollView setContentOffset: CGPointMake(0, scrollView.contentOffset.y)];
}
Another way would be having a UIScrollView which is smaller or equal to the size of its parent view and with a disabled "Always bounce horizontal".
The safest and most successful method I've found to constrain the movement of a scroll view is to subclass UIScrollView and override setContentOffset:animated: and setContentOffset: methods (code below).
The advantage of overriding these methods is that it directly alters the requested contentOffset before any of the UIKit code starts to act on it, avoiding any of the side effects that can occur when modifying the contentOffset in scrollViewDidScroll: or other UIScrollViewDelegate methods.
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
// restrict movement to horizontal only
CGPoint newOffset = CGPointMake(contentOffset.x, 0);
[super setContentOffset:newOffset animated:animated];
}
- (void)setContentOffset:(CGPoint)contentOffset {
// restrict movement to horizontal only
CGPoint newOffset = CGPointMake(contentOffset.x, 0);
[super setContentOffset:newOffset];
}
i guess i would use the method scrollViewWillBeginDragging
found in the UIScrollViewDelegate
and inside i could control if user is going horizontally or not...
scrollViewWillBeginDragging:
Tells the delegate when the scroll view is about to start scrolling the content.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
Parameters
scrollView
The scroll-view object that is about to scroll the content view.
Discussion
The delegate might not receive this message until dragging has occurred over a small distance.
Availability
Available in iOS 2.0 and later.
I created a zoomable UIScrollView and added 100 subviews to it (tiled). The view scrolls perfectly left and right. However, I'd like to allow zooming.
To do so I read that my delegate needs to implement:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return ???;
}
I have seen examples that have only one subview to zoom, so they return that subview in that method. In my case, however, I have a lot more. What is the proper way to do the zooming?
I tried creating another UIView and adding the 100 subviews to that one. And then return that one view on the method above, but I doesn't work (it zooms but once it stops, it's not interactive any more).
Exactly,
This is what Apple also is mentioning in the Scroll View Programming Guide:
Just create a view, add all subviews to this view and add the newly created view as a single subview to the scrollview...
Then in the viewForZoomingInScrollView delegate method return the object at Index 0:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return [self.scrollView.subviews objectAtIndex:0];
}
I created the view where I added everything using:
UIView *zoomableView = [[UIView alloc] init];
without setting its frame.
The problem was solved when, after adding all the subviews to it, I set its frame to something large enough to accommodate all the tiled subviews.
You have to return what your going to add views to scroll view as a subviews.
Ex: If you are adding image view to scroll view then write
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return imageView object;
}