How to properly implement a draggable "drawer" custom UIView - ios

I have a custom UIView at the bottom on my view-controller that acts as a "drawer" control. About 90 pixels of the view is visible. When a user taps the view, it animates upward revealing other options, controls, etc. After another tap, the view animates back down to its original position. Effectively "closing the drawer".
I've also implemented code so that the view tracks with a dragging gesture:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y >= 80) {
UITouch *theTouch = [touches anyObject];
CGPoint location = [theTouch locationInView:self];
CGPoint previousLocation = [theTouch previousLocationInView:self];
CGFloat target = location.y - previousLocation.y;
self.frame = CGRectOffset(self.frame, 0, target);
}
}
All of this is working as expected. However, the view is not responding correctly to a quick "flick" gesture. In the touchesEnded:withEvent: method I'm animating the view from where the user lifted his/her finger to its destination. This works well if the user drags slowly then releases. But if they flick quickly, the view still has to process a 0.5 second duration which isn't smooth to the eye. Here's all of the custom view's code:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y >= 80) {
UITouch *theTouch = [touches anyObject];
CGPoint location = [theTouch locationInView:self];
CGPoint previousLocation = [theTouch previousLocationInView:self];
CGFloat target = location.y - previousLocation.y;
self.frame = CGRectOffset(self.frame, 0, target);
}
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y == kDefaultUpY) {
_comingFromUp = YES;
_comingFromDown = NO;
}
else if(self.frame.origin.y == kDefaultDownY) {
_comingFromUp = NO;
_comingFromDown = YES;
}
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//responding to tap
if(self.frame.origin.y == kDefaultUpY) {
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
return;
}
else if(self.frame.origin.y == kDefaultDownY) {
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
return;
}
//responding to drag
if(_comingFromDown) {
if(self.frame.origin.y < kDefaultDownY) {
//dragging up -- go up
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
else if(self.frame.origin.y > kDefaultDownY) {
//dragging down -- stay down
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
}
else if(_comingFromUp) {
if(self.frame.origin.y < kDefaultUpY) {
//dragging up -- stay up
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
else if(self.frame.origin.y > kDefaultUpY) {
//dragging down -- go down
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
}
}
How can I properly respond to the gestures associated with a "drawer" view? Is there a standard way of achieving this kind of functionality?

Use gesture recognizers rather than implementing touch handling code yourself. You can attach both a swipe gesture recognizer and a pan gesture recognize, and set up the pan gesture so the swipe has to fail before the pan is triggered. Make the animation for the swipe gesture be faster. Something like 0.1 or 0.2 seconds is probably right for a swipe gesture.
Something to watch out for, though: In iOS 7, a swipe gesture from the bottom of the screen triggers the system settings drawer to come up from the bottom of the screen. Our app Face Dancer has a controls drawer, and we recently discovered that under iOS 7 its hard to slid our controls drawer up without triggering the system settings drawer instead. We have a "thumb" region for dragging, and we are adding a tap gesture that will fully display it/hide it in one step.

Related

UIView Animation Cancel

I want to make a UIView go off the screen upon a touch and come back after a small delay after touch up. So far:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if ([touch view] == myMainView)
{
[disappearingView.layer removeAllAnimations];
[UIView animateWithDuration:0.5
delay:0
usingSpringWithDamping:0.7
initialSpringVelocity:0.2
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
disappearingView.transform = CGAffineTransformMakeScale(0, 0);
}
completion:^(BOOL finished)
{
}];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if ([touch view] == myMainView)
{
[UIView animateWithDuration:0.5
delay:1
usingSpringWithDamping:0.7
initialSpringVelocity:0.2
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
disappearingView.transform = CGAffineTransformMakeScale(1, 1);
}
completion:^(BOOL finished)
{
}];
}
}
Above code works fine so far. However, if the user lift the finger up and touch down again before the 1 second delay expires, disappearing view still comes back, even with the touch down. If you touch down and up randomly, animation is very inconsistent.
I want view to come back only after 1 second and there are no touches at all.
Set a flag in touchesBegan and clear it again in touchesEnded. In your animation block for touchesEnded, check the flag and do not rescale to (1,1) if the flag is set. That way if you get a second touch after the first has ended but before the animation completes, it will not come back.

MKMapView pin drag from another view

I'm thinking about a Google Map street view pin drag and drop system on iOS. The user selects a pin from another view (let's say UINavigationBar) and drag it to a position on map and then drop it.
I'm a bit lost with it. Do you have a workflow for this type of interaction ?
It is quite a complex task but it can be divided into a couple of simple steps.
Make a custom UIView, which after touching will follow the touch movement.
Example
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_originalPosition = self.view.center;
_touchOffset = CGPointMake(self.view.center.x-position.x,self.view.center.y-position.y);
}
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint position = [touch locationInView: self.view.superview];
[UIView animateWithDuration:.001
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
self.view.center = CGPointMake(position.x+_touchOffset.x, position.y+_touchOffset.y);
}
completion:^(BOOL finished) {}];
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint positionInView = [touch locationInView:self.view];
CGPoint newPosition;
if (CGRectContainsPoint(_desiredView.frame, positionInView)) {
newPosition = positionInView;
// _desiredView is view where the user can drag the view
} else {
newPosition = _originalPosition;
// its outside the desired view so lets move the view back to start position
}
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
self.view.center = newPosition
// to
}
completion:^(BOOL finished) {}];
}
When user releases the finger, you will have to get the location of the touch.
Calculate the touch location in map view coordinates and place the pin.
Hope it points you to the right direction.

(iOS) Drag and drop, Snap to grid, Check proper area

I've read all the stuff about drag'n'drop and gestureRecognizer but haven't found the solution.
I got "gamefield" 10x10 and I want to implement drag'n'drop with snap to grid and check if element placed on "gamefield".
I got my 8 "TempButtons" added to UIView buttonsSuperView with action:#selector(imageMoved:withEvent:).
- (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
originalPosition = [self getButton:sender];
UITouch *touch = [[event allTouches] anyObject];
point = [touch locationInView:control];
float step = 31.0; // Grid step size.
CGPoint center = control.center;
center.x += step * floor((point.x / step));
center.y += step * floor((point.y / step));
control.center = center;
}
It works, I can move any button with grid step 31px. I can place it everywhere, but I have to put it on UIView fieldView. If it placed right there it's OK and I can replace it on another tile. But if I place it outside fieldView the button should return on its place. Something like this:
if ([self.fieldView pointInside:point withEvent:nil])
{
control.center = center;
} else {
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
control.center = originalPosition;
// to
}
completion:^(BOOL finished) {}];
}
If this code placed on - (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event method it makes button get back to its "originalposition" all the time its moving. But I need to check if its placed properly only on 'touchesEnded'. But that's another method and I don't understand how to realize which button pressed and its original coords to get back if placed improperly.
Maybe there's a better way to use 'UIView' instead of 'UIButton' but I still don't understand how to do that.
Thanks in advance.
Kind Regards,
Nick
Since no one answered my question I've spent some time and found working solution.
- (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event
{
dragObject = sender;
originalPosition = [self getButton:sender];
UITouch *touch = [[event allTouches] anyObject];
point = [touch locationInView:dragObject];
float step = 31.0; // Grid step size.
CGPoint center = dragObject.center;
center.x += step * floor((point.x / step));
center.y += step * floor((point.y / step));
dragObject.center = center;
[sender addTarget:self action:#selector(touchesEnded:withEvent:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (![fieldView pointInside:dragObject.center withEvent:nil])
{
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
dragObject.center = CGPointMake(originalPosition.x + 15.5, originalPosition.y + 15.5);
}
completion:^(BOOL finished) {}];
}
}
Now it works like charm and I can move along. My buttons moving freely snapped to grid and when pleced outside game field they move back to their original position.
Regards,
Nick

If I'm making a UITableViewCell slideable (for quick delete) how do I make it so it's still tappable?

I've successfully made it that my UITableViewCell can be slid right and left to mark the cell as read, or to delete it (sort of like how the Reeder app does it) but now it doesn't allow me to simply tap it. How do I allow it to be tapped as well?
I used this article for the structure of the concept, so more information is available there.
I implemented the sliding as follows:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_firstTouchPoint = [[touches anyObject] locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = [[touches anyObject] locationInView:self];
// Holds the value of how far away from the first touch the finger has moved
CGFloat xPos;
// If the first touch point is left of the current point, it means the user is moving their finger right and the cell must move right
if (_firstTouchPoint.x < touchPoint.x) {
xPos = touchPoint.x - _firstTouchPoint.x;
if (xPos <= 0) {
xPos = 0;
}
}
else {
xPos = -(_firstTouchPoint.x - touchPoint.x);
if (xPos >= 0) {
xPos = 0;
}
}
// Change our cellFront's origin to the xPos we defined
CGRect frame = self.cellFront.frame;
frame.origin = CGPointMake(xPos, 0);
self.cellFront.frame = frame;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[self springBack];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self springBack];
}
- (void)springBack {
CGFloat cellXPositionBeforeSpringBack = self.cellFront.frame.origin.x;
CGRect frame = self.cellFront.frame;
frame.origin = CGPointMake(0, 0);
[UIView animateWithDuration:0.1 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.cellFront.frame = frame;
} completion:^(BOOL finished) {
}];
if (cellXPositionBeforeSpringBack >= 80 || cellXPositionBeforeSpringBack <= -80) {
NSLog(#"YA");
}
}
When I tap it though, nothing happens.
Just use UIPanGestureRecognizer.
the touchesBegan approach is so old.
With the pan Gesture you will not lose the touch event on your cell.
The easiest to solve it, is actually to build your own gesture for sliding right and left.
After you build it, just add this gesture to the view, and add the tap gesture as well.
If you allow the both gestures (tap gesture and slide gesture) to live simultaneously by implementing the 'shouldRecognizeSimultaneouslyWithGestureRecognizer' method, iOS will take care of it for you very easily.
Here how to build custom gesture recogniser:
http://blog.federicomestrone.com/2012/01/31/creating-custom-gesture-recognisers-for-ios/

Move UIView left to right while they move down the screen

I am trying to create an iOS app with blocks floating down the screen(UIViews). I have them floating down the screen but I also want the user to be able to move them on the x axis as they are falling. I tried to do it with the code below but they just fall and don't move left to right. My problem is I am trying to move it with my finger left to right as it is already moving town the screen. How can I adapt the code below to work?
Note: I was able to move the views left to right without them moving down the screen and I was able to move them down the screen without moving them left to right. The problem arises when I combine both.
ANIMATION FOR Y AXIS
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:letView.speed];
[UIView setAnimationDelay:0.0];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
letView.layer.frame = CGRectMake(letView.layer.frame.origin.x, [[UIScreen mainScreen] bounds].size.height, letView.layer.frame.size.width, letView.layer.frame.size.height);
[UIView commitAnimations];
ANIMATION FOR TOUCH
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
//Goes through an array of views to see which one to move
for (LetterView * view in _viewArray) {
if (CGRectContainsPoint(view.frame, touchLocation)) {
dragging = YES;
currentDragView = view;
[currentDragView.layer removeAllAnimations];
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
if (dragging) {
CGPoint location = touchLocation;
currentDragView.center = CGPointMake(location.x, currentDragView.center.y);
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
dragging = NO;
[self checkForCorrectWord];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:currentDragView.speed];
[UIView setAnimationDelay:0.0];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
currentDragView
.layer.frame = CGRectMake(currentDragView.layer.frame.origin.x, [[UIScreen mainScreen] bounds].size.height, currentDragView.layer.frame.size.width, currentDragView.layer.frame.size.height);
[UIView commitAnimations];
}
I've got a demo project that shows you how to make a "boat" animate down the screen in an s-curve, like this:
The solution I use is to make a keyframe animation (actually it's a keyframe animation that forms part of a grouped animation, but you might not need the rest of the grouped animation: it is the keyframe animation that makes the curved path shape). Perhaps you could adapt what I'm doing, modifying it to your own purposes?
The code is available for download here:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch17p501groupedAnimation
It is discussed in detail in my book. Keyframe animations generally:
http://www.apeth.com/iOSBook/ch17.html#_keyframe_animation
This particular example:
http://www.apeth.com/iOSBook/ch17.html#_grouped_animations

Resources