I need to implement a drag and drop feature for a UIView added as a subview of UITableViewCell. I catch the drag according to MoveMe example from Apple by using touchesBegan: touchesMoved: events.
This is the view that is added inside the cell:
The gradientview is the subview of the cell. I need to move the white rectangles only (white boxes are the subviews of the orange-blue gradient view). My problem is that after a few pixels of upward or downward dragging the tableview starts to scroll. Horizontaly it is OK. Seems that the tableView catches the drag after a certain amount of drag. I need to prevent the vertical scroll until the drag is ongoing.
Is there a solution to prevent this from happening?
OK. I ended up using UIPanGestureRecognizer. That seems to block tableview underneath.
- (void)move:(UIPanGestureRecognizer*) recognizer {
CGPoint translation = [recognizer translationInView:self];
[recognizer setTranslation:CGPointMake(0, 0) inView:self];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
}
Related
I have been trying to forward touch events from one view to a UITableView. But I have come across a problem.
In the figure is a prototype view to illustrate the problem. The white view in the bottom half is a normal UITableView. The yellow view is a UIView that forwards touches to the middle of the UITableView using hitTest:withEvent:. The red view is a UIButton.
The goal is to forward the up and down panning touches from the yellow view to the UITableView at the bottom. However, tapping the red UIButton should still work.
I have tried 2 main approaches both of which have problems.
The first approach is to add UIPanGestureRecognizer to the yellow view that can be used to set the contentOffset of the UITableView. The problem is that inertial scrolling of the table view does not work with this approach.
The second approach is to forward touches using hitTest:withEvent: in the yellow view.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if([self.button_red pointInside: [self convertPoint:point toView:self.button_red] withEvent:event]) {
return self.button_red;
} else {
return [self.view_table hitTest:CGPointMake(self.view_table.width/2.0,self.view_table.height/2.0) withEvent:event];
}
}
hitTest:withEvent: is only called when the touch starts in the yellow view. In other words, whenever a drag starts in the red view it won't forward the drag to the table view. Which is not good enough.
Is it possible to forward a pan gesture from the yellow view to the UITableView in a different way that keeps the red button working AND allows drags to work that start in the red button? Should I override the touch event methods in the yellow view to get this to work somehow? Is there way to do all this while keeping the inertial scrolling?
You need to add yellowView as a subview inside the UITableView.
You will also need to take care of the contentInset in order to
accommodate for yellowView height.
By default, cells will appear over this added yellowView, you need
to fix this by adjusting zPosition on the yellowView.
CGFloat height = CGRectGetHeight(yellowView.frame);
// fix yellowView's y position
yellowView.frame = CGRectMake(0, -height, CGRectGetWidth(yellowView.frame), height);
[tableView addSubview:yellowView];
yellowView.layer.zPosition = 10; // make it appear over cells
[tableView setContentInset:UIEdgeInsetsMake(height, 0, 0, 0)];
Hope this helps.
I have two nested UIScrollViews, both scrolling in the vertical direction. I need the outer scrollview to scroll to it's max range first before allowing the inner scrollview to scroll. The inner scrollview should not be scrollable until the outer scrollview has reached it's max range. Here's an illustration:
In the left diagram, a vertical drag inside of Scrollview B should move Scrollview A and Scrollview B should not be scrollable (but it still needs to be able to receive touches/taps). Once Scrollview A reaches it's max range (when Scrollview B gets to the top of the screen), then Scrollview B should scroll. This needs to work in one continuous motion.
I've attempted to toggle ScrollView B's scrollEnabled from ScrollView A's scrollViewDidScroll: delegate method, but this doesn't appear to be a viable solution because it doesn't work in one continuous motion (eg: The user needs to release and touch again after Scrollview B reaches the top of the screen).
What's the best way to implement this such that is works in one continuous motion?
I solved the problem in the following way. I am not really happy with it since it looks to me much too complicated, but it works (please note that the code below is a simplified, untested version of my code, which is due to a different UI more complicated):
I have 3 properties that control scrolling:
#property (nonatomic, assign) CGFloat currentPanY;
#property (nonatomic, assign) BOOL scrollA;
#property (nonatomic, assign) BOOL scrollB;
2-step scrolling:
Disable scrolling for B, and enable scrolling for A.
This allows to scroll A .
When A reaches its max position, disable scrolling for A, and enable scrolling for B:
-(void)scrollViewDidScroll: (UIScrollView *)scrollView {
if (scrollView.contentOffset.y >= self.maxScrollUpOffset) {
[scrollView setContentOffset:CGPointMake(0, self.maxScrollUpOffset) animated:NO];
self.scrollviewA.scrollEnabled = NO;
self.scrollviewB.scrollEnabled = YES;
self.scrollB = YES;
}
}
This gives the following effect:
When A is scrolled upwards it will stop scrolling when its max size is reached. However B will not start scrolling, since the pan gesture recognizer of A does not forward its actions to the pan gesture recognizer of B. Thus one has to lift the finger and to start a 2nd scrolling. Then, B will scroll. This gives the 2-step scrolling.
Continuous scrolling:
For continuous scrolling, B must scroll while the finger that started scrolling of A continues moving upwards. To detect this, I added a further pan gesture recognizer tho A, and enabled it to detect gestures simultaneously with the built-in gesture recognizers of A and B:
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer {
return YES;
}
In the action of this additional pan gesture recognizer, I compute the distance the finger has moved upwards after the scrolling limit of A has been reached. By this distance, B is then scrolled programmatically:
- (void)panGestureRecognizerAction:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateChanged) {
self.currentPanY = 0;
self.scrollB = NO;
self.scrollA = NO;
} else {
CGPoint currentTranslation = [recognizer translationInView:self.scrollviewA];
CGFloat currentYup = currentTranslation.y;
if (self.scrollA || self.scrollB) {
if (self.currentPanY == 0) {
self.currentPanY = currentYup;
}
CGFloat additionalYup = self.currentPanY - currentYup;
if (self.scrollA) {
CGFloat offset = self.scrollviewA.scrollUpOffset + additionalYup;
if (offset >= 0) {
self.scrollviewA.contentOffset = CGPointMake(0, offset);
} else {
self.scrollviewA.contentOffset = CGPointZero;
}
} else if (self.scrollB){
self.scrollviewB.contentOffset = CGPointMake(0, additionalYup);
}
}
}
}
There is still a disadvantage:
If you start scrolling, lift the finger, and let the scrollView decelerate, it will behave like the 2-stage scrolling, since the additional pan gesture recognizer won’t recognize any pan gesture.
In my case I solved subclassing UIScrollView for the outer ScrollView.
class MYOuterScrollView: UIScrollView, UIGestureRecognizerDelegate
{
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
}
Your requirement that this should work in one continuous motion calls out the answer: you need to use just one UIScrollView, not two.
If you have just one scrollview you can then perform your magic by overriding the scrollview's layoutSubviews method and rejigging its content to perform your parallax effect based on the current value of contentOffset. Make sure that contentSize always reflect the full height (you can even update contentSize inside layoutSubviews if needs be).
For an architecture, take your existing diagram and just replace Scrollview B with View B.
The gesture recognizer for scroll view A would need to pass off to the gesture recognizer for scroll view B to have on continuous motion which I am pretty sure is not possible. Why not combine the content of the two scroll views instead and then you would have one continuous motion. This code will combine the content of scrollView A and B into just A.
UIScrollView* scrollViewA = ...;
UIScrollView* scrollViewB = ...;
NSArray* subviews = scrollViewB.subviews;
for (int i = 0; i < subviews.count; i++)
{
UIView* subview = [subviews objectAtIndex:i];
CGRect frame = subview.frame;
frame.origin.y += scrollViewA.contentSize.height;
subview.frame = frame;
[scrollViewA addSubview:subview];
}
CGSize size = scrollViewA.contentSize;
size.height += scrollViewB.contentSize.height;
scrollViewA.contentSize = size;
Same-Direction Scrolling
Same direction scrolling occurs when a UIScrollView that is a subview of a UIScrollView both scroll in the same direction. This is shown in the left image.
Follow this link if want to look in details https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html
Just set some attributes which are mentioned in above image. it works.
I have a draggable view (UIImageView) which I control its positioning and dragging using the UIPanGestureRecognizer and this method:
- (void)imageDragged:(UIPanGestureRecognizer *)gesture
{
UIImageView *img = (UIImageView *)gesture.view;
CGPoint translation = [gesture translationInView:img];
img.center = CGPointMake(img.center.x + translation.x,
img.center.y + translation.y);
// reset translation
[gesture setTranslation:CGPointZero inView:img];
}
Other than the draggable view, I also have a UICollectionView, built of course by UICollectionViewCells. What Im trying to do, is to identify when my draggable view is dragged on top of one of the collection view cells.
I thought of adding a boolean to the imageDragged method, to know when the view is currently being dragged, but I wasn't able to figure out how to know when my dragged touch is on top of a collection view cell. The UICollectionView has the didSelect delegate method, but that doesn't really help me if my tap didn't occur on the cells, but on my draggable view.
Any help would be much appreciated!
I put this snippet inside the imageDropped method to achieve what I wanted (thanks to Martin's help):
CGPoint tapCoordinates = [gesture locationInView:self.view];
NSArray * visibleCells = [self.collectionView visibleCells];
for(UICollectionViewCell *cell in visibleCells) {
CGRect frame = [self.collectionView convertRect:cell.frame toView:self.view];
if(CGRectContainsPoint(frame, tapCoordinates)) {
NSLOG(#"Success");
break;
}
}
I don't know if there's a tidier way, but perhaps you can get the point of the tap and/or some points on the corners of the dragged UIView and then use CGRectContainsPoint to see if those points are within the rect of the collection view cell.
Example:
You have a UIScrollView with scrollEnabled = NO.
After user pans a certain distance, you want the scroll view to start scrolling.
When finger goes down on screen and I disable scrolling, the scroll view is dead until the finger is lifted. Enabling scrolling in middle of a pan doesn't cause it to start.
I suspect it is because UIScrollView will decide to ignore the touch sequence if it was scrollEnabled=NO during its internal touchesBegan:withEvent: (or its private pan gesture recognizer).
How would you trick it into starting to scroll natively (with bounce effect) in the middle of a touch sequence?
Reason: For a simple UIKit based game that requires this mechanic.
Okay, this still needs a little bit of tweaking to handle scrolling again after the first scroll has stopped, but I think the key to what you're trying to achieve is to implement the UIScrollViewDelegate method scrollViewDidScroll:. From here, you can save the starting content offset of the scroll view, and compare the scroll view's current offset to the starting offset added to the y translation of the scroll view's pan gesture recognizer.
The code below successfully doesn't allow scrolling to start until the user has panned at least 100 pts. Like I said, this needs a little tweaking, but should be enough to get you going.
- (void)viewDidLoad
{
[super viewDidLoad];
[self setStartingOffset:CGPointMake(0.0, -1.0)];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.startingOffset.y < 0.0) {
[self setStartingOffset:scrollView.contentOffset];
}else{
static CGFloat min = 100.0;
CGFloat yTranslation = fabs([scrollView.panGestureRecognizer translationInView:scrollView].y);
if (yTranslation < self.startingOffset.y + min) {
[scrollView setContentOffset:self.startingOffset];
}else{
if (self.startingOffset.y >= 0.0) {
[self setStartingOffset:CGPointMake(0.0, -1.0)];
}
}
}
}
I have a UIScrollView that on it I have also created Gesture Recognizers. Tap, Double Tap, two finger tap, etc. On the Scroll View I create several other UIViews. Think of each of these views as Drawing objects. Circle, Squares, buttons, images, etc. Each of the Subviews I can pan, rotate, tap, etc and they all work for the most part.
If the Scaling Scroll View is not at 100% (1-1) then panning the subviews gets a bit sketchy. you can always tap them to get them to Highlight, though panning, rotation, etc is iffy. Typically if I try to pan a selected subview, it pans the scroll view. Sometimes it works, sometimes it does not. Set the zoom to 100%, or turn off the Scrolling (set scale to the same min/max), I can do what Is expected.
Any Suggestions on where to start troubleshooting this?
Not 100% why this works, but this is the code that made the issue disappear. I had to Subclass UIScrollView, override (BOOL)touchesShouldCancelInContentView:(UIView *)view and return NO if the view was anything but the UIView class for the UIScrollView.View
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
BOOL returnVal = NO;
if ([view isKindOfClass:[IoScreenEditorContentView class]]) {
returnVal = [super touchesShouldCancelInContentView:view];
}
return returnVal;
}