Subviews moving out of place when parent view is rotated - ios

I'm following this guide here: http://guti.in/articles/creating-tinder-like-animations/ in order to recreate a Tinder-like swiping card effect.
Long story short, I have a UIView with a PanGestureRecognizer set to do the following:
- (void)dragged:(UIPanGestureRecognizer *)gestureRecognizer
{
CGFloat xDistance = [gestureRecognizer translationInView:self].x;
CGFloat yDistance = [gestureRecognizer translationInView:self].y;
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
self.originalPoint = self.center;
break;
};
case UIGestureRecognizerStateChanged:{
CGFloat rotationStrength = MIN(xDistance / 320, 1);
CGFloat rotationAngel = (CGFloat) (2*M_PI * rotationStrength / 16);
CGFloat scaleStrength = 1 - fabsf(rotationStrength) / 4;
CGFloat scale = MAX(scaleStrength, 0.93);
self.center = CGPointMake(self.originalPoint.x + xDistance, self.originalPoint.y + yDistance);
CGAffineTransform transform = CGAffineTransformMakeRotation(rotationAngel);
CGAffineTransform scaleTransform = CGAffineTransformScale(transform, scale, scale);
self.transform = scaleTransform;
break;
};
case UIGestureRecognizerStateEnded: {
[self resetViewPositionAndTransformations];
break;
};
case UIGestureRecognizerStatePossible:break;
case UIGestureRecognizerStateCancelled:break;
case UIGestureRecognizerStateFailed:break;
}
}
- (void)resetViewPositionAndTransformations
{
[UIView animateWithDuration:0.2
animations:^{
self.center = self.originalPoint;
self.transform = CGAffineTransformMakeRotation(0);
}];
}
However, unlike the view in the example, mine has multiple different subviews, and I'm finding that when I apply this code, I get this erroneous result:
Before Swipe:
During Swipe:
As you can see, it seems as if the subviews are "sliding" out of place, so to speak. What's worse, when the swipe is over, the subviews are left looking slightly out of place, even though the parent view is in the right place. If you're curious, my constraints look like:
If anyone could help me get the subviews to move correctly with the superview, I would really appreciate it. One thing I've found sort-of remedies the last problem is to store the original centers of each of the subviews and restore them to that at the end, but I'm sure there must be a better way.
EDIT: I'm still haven't come up with a fix, but I'm now 99.9% sure it's to do with the constraints of the subviews.

Related

Assign proper velocity to pan gesture iOS?

I have a tableView with a panGesture. I am able to pan this tableView from bottom part of the screen vertically. However, I want the panGesture to continue with a certain velocity, to get a scrollView kind of effect while panning up/down.
What happens is that the tableView pans very quickly in upwards or downwards direction with no sign of stopping.
Here's what I have done so far :
-(void)handlePanWithVelocity:(UIPanGestureRecognizer *)gestureRecognizer{
CGPoint displacement;
CGPoint velocity = [gestureRecognizer velocityInView:self.mapView];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
break;
};
case UIGestureRecognizerStateChanged:{
CGPoint translation = [gestureRecognizer translationInView:self.view];
displacement = (isVerticalPan) ? CGPointMake(0, translation.y) : CGPointMake(translation.x, 0);
mytableView.transform = CGAffineTransformMakeTranslation(displacement.x, displacement.y);
break;
};
case UIGestureRecognizerStateEnded: {
displacement.y = displacement.y + velocity.y;
[UIView animateWithDuration:1 animations:^{
mytableView.transform = CGAffineTransformMakeTranslation(displacement.x, displacement.y);
}];
break;
};
case UIGestureRecognizerStatePossible:break;
case UIGestureRecognizerStateCancelled:break;
case UIGestureRecognizerStateFailed:break;
}
}
What can be done to improve the current behaviour?
The thing is that you dont measure the time that it takes to move in the first place. Since velocity it x/t you need to adjust the duration of you UIView animation. You have to measure the time between the Pan Recognizer callbacks and then calculate the speed.
global variables:
double velocity;
NSDate *date;
and then:
-(void)handlePanWithVelocity:(UIPanGestureRecognizer *)gestureRecognizer{
CGPoint displacement;
CGPoint velocity = [gestureRecognizer velocityInView:self.mapView];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
break;
};
case UIGestureRecognizerStateChanged:{
CGPoint translation = [gestureRecognizer translationInView:self.view];
displacement = (isVerticalPan) ? CGPointMake(0, translation.y) : CGPointMake(translation.x, 0);
mytableView.transform = CGAffineTransformMakeTranslation(displacement.x, displacement.y);
if(date){
double timePassed_ms = [date timeIntervalSinceNow] * -1000.0;
velocity = displacement.y/timePassed_ms;
}
date = [NSDate date];
break;
};
case UIGestureRecognizerStateEnded: {
displacement.y = displacement.y + velocity.y;
[UIView animateWithDuration:1 animations:^{
mytableView.transform = CGAffineTransformMakeTranslation(0, velocity);
}];
break;
};
case UIGestureRecognizerStatePossible:break;
case UIGestureRecognizerStateCancelled:break;
case UIGestureRecognizerStateFailed:break;
}
}
I didnt test the code but you get the general idea.

Pangesture Fails and then stuck on view

My app consists of a deck of cards. Each card has a scrollview that allows you to look at images associated with that card. You can drag to look at another card in the deck via pan gesture. Unfortunately, if you don't do a clean drag the first time (like if you drag half way and then lift your finger off), the app gets stuck on that card. You can scroll up and down, but you won't be able to drag. When you try, you just move backgroundScrollView around within the superview(so swiping right means you see the grey, right margins between scrollview and superview).
This is the method that UIPanGestureRecognizer calls:
-(void)beingDragged:(UIPanGestureRecognizer *)gestureRecognizer {
NSLog(#"I am being dragged");
xFromCenter = [gestureRecognizer translationInView:self].x; //%%% positive for right swipe, negative for left
yFromCenter = [gestureRecognizer translationInView:self].y; //%%% positive for up, negative for down
switch (gestureRecognizer.state) {
//%%% just started swiping
case UIGestureRecognizerStateBegan:{
self.backgroundScrollView.scrollEnabled = NO;
self.originalPoint = self.center;
_backgroundScrollView.autoresizingMask = UIViewAutoresizingNone;
_backgroundScrollView.translatesAutoresizingMaskIntoConstraints = YES;
likeBadge.autoresizingMask = UIViewAutoresizingNone;
likeBadge.translatesAutoresizingMaskIntoConstraints = YES;
passBadge.autoresizingMask = UIViewAutoresizingNone;
passBadge.translatesAutoresizingMaskIntoConstraints = YES;
reviewButton.autoresizingMask = UIViewAutoresizingNone;
reviewButton.translatesAutoresizingMaskIntoConstraints = YES;
break;
};
//%%% in the middle of a swipe
case UIGestureRecognizerStateChanged:{
//%%% dictates rotation (see ROTATION_MAX and ROTATION_STRENGTH for details)
CGFloat rotationStrength = MIN(xFromCenter / ROTATION_STRENGTH, ROTATION_MAX);
//%%% degree change in radians
CGFloat rotationAngel = (CGFloat) (ROTATION_ANGLE * rotationStrength);
//%%% amount the height changes when you move the card up to a certain point
CGFloat scale = MAX(1 - fabsf(rotationStrength) / SCALE_STRENGTH, SCALE_MAX);
//%%% move the object's center by center + gesture coordinate
self.center = CGPointMake(self.originalPoint.x + xFromCenter, self.originalPoint.y + yFromCenter);
//%%% rotate by certain amount
CGAffineTransform transform = CGAffineTransformMakeRotation(rotationAngel);
//%%% scale by certain amount
CGAffineTransform scaleTransform = CGAffineTransformScale(transform, scale, scale);
//%%% apply transformations
self.transform = scaleTransform;
[self updateOverlay:xFromCenter:yFromCenter];
break;
};
//%%% let go of the card
case UIGestureRecognizerStateEnded: {
self.backgroundScrollView.scrollEnabled = YES;
[self afterSwipeAction];
break;
};
case UIGestureRecognizerStatePossible:{
self.backgroundScrollView.scrollEnabled = YES;
};
case UIGestureRecognizerStateCancelled:{
self.backgroundScrollView.scrollEnabled = YES;
};
case UIGestureRecognizerStateFailed:{
self.backgroundScrollView.scrollEnabled = YES;
};
}
}
My solution has been to set
_backgroundScrollView.translatesAutoresizingMaskIntoConstraints = NO;
whereas before it was set to YES. This does mean that scrollView now leaks out of the view when you swipe, but that's for another post...

Scale animation in background from another anchorPoint

Ive done a Scaling animation for background image with an arrow in it,well by scaling the image(it includes the arrow) meaning its a whole background. it scales from the center of the background. but i want to start scaling from the center of arrow.
i want to achieve this :
Then to become with scale:
What i have to change in my code:
self.blueBackground.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
[UIView animateWithDuration:5 animations:^{
self.blueBackground.transform = CGAffineTransformScale(CGAffineTransformIdentity, 30, 30);
} completion:^(BOOL finished) {
}];
With my current code the scale start from center of background so the arrow end up at the right (out the bounds of screen).
Try something like this:
- (void)animateArrow
{
self.blueBackground.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:5 animations:^{
ScaleViewAboutPointInBounds(self.blueBackground, arrowCenter, 30);
} completion:^(BOOL finished) {
}];
}
static void ScaleViewAboutPointInBounds(UIView *view, CGPoint point, CGFloat scaleAmount)
{
CGFloat xAnchor = view.layer.anchorPoint.x * CGRectGetWidth(view.bounds);
CGFloat yAnchor = view.layer.anchorPoint.y * CGRectGetHeight(view.bounds);
CGFloat xOffset = point.x - xAnchor;
CGFloat yOffset = point.y - yAnchor;
CGAffineTransform shift = CGAffineTransformMakeTranslation(-xOffset, -yOffset);
CGAffineTransform unshift = CGAffineTransformMakeTranslation(xOffset, yOffset);
CGAffineTransform scale = CGAffineTransformMakeScale(scaleAmount, scaleAmount);
view.transform = CGAffineTransformConcat(shift, CGAffineTransformConcat(scale, unshift));
}
It's probably more general than what you need, and you could do it simpler, but this should work too. Just change the point to something that looks like the center of the arrow.

How to stick UIView to top of UIWindow on iOS 7

I have a custom UIView which I want to stick to the top of the UIWindow even when interface orientation changes. Here is the my view in portrait orientation
The problem is that prior iOS 8 UIWindow coordinate system is not being changed with the orientation changes. So I need to make all the calculations by hand.
The first thing is to change the transform of the UIView, which I do using this method
-(CGFloat)angleForOrientation:(UIInterfaceOrientation)orientation
{
CGFloat angle;
switch (orientation) {
case UIInterfaceOrientationLandscapeLeft:
angle = -M_PI /2.0;
NSLog(#"UIInterfaceOrientationLandscapeLeft");
break;
case UIInterfaceOrientationLandscapeRight:
angle = M_PI /2.0;
NSLog(#"UIInterfaceOrientationLandscapeRight");
break;
case UIInterfaceOrientationPortraitUpsideDown:
angle = M_PI;
NSLog(#"UIInterfaceOrientationPortraitUpsideDown");
break;
default:
angle = 0;
NSLog(#"UIInterfaceOrientationPortrait");
break;
}
return angle;
}
The second thing is to somehow map actual coordinate systems to UIWindow coordinate system, which stays the same.
So how should I calculate the frame of the custom UIView that even when the user rotates the view to other orientations I will have the same sized UIView sticked to the top centre of the screen?
For instance this is how should the view look like in landscape
By the way this image is generated from iOS 8 version. For which I do the following
self.frame = CGRectMake(window.bounds.size/2-50, 0, 100, 100);
CGFloat angle = [self angleForOrientation:orientation];
self.transform = CGAffineTransformMakeRotation(angle);
I need to do something similar to iOS 7. How can I achieve this?
Thanks for help!
So Finally I figured out how to implement this.
I've created the following method to calculate the rect which will stick the view to top based on given bounds.
-(CGRect)getTopRectForBounds:(CGRect)bounds orientation:(UIInterfaceOrientation)orientation window:(UIWindow *)window
{
CGRect newRect;
CGFloat statusBarHeight = [self getStatusBarHeight];
CGSize screenSize = window.screen.bounds.size;
CGFloat sW = screenSize.width;
CGFloat sH = screenSize.height;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
switch (orientation) {
case UIInterfaceOrientationLandscapeLeft:
newRect = CGRectMake(statusBarHeight, (sH-W)/2, H,W);
break;
case UIInterfaceOrientationLandscapeRight:
newRect = CGRectMake(sW-H-statusBarHeight, (sH-W)/2, H,W);
break;
case UIInterfaceOrientationPortraitUpsideDown:
newRect = CGRectMake((sW-W)/2, sH-H-statusBarHeight, W,H);
break;
default:
newRect = CGRectMake((sW-W)/2, statusBarHeight, W,H);
break;
}
return newRect;
}
Then I just change the frame whenever orientation changes.
So first I listen to Orientation changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationChanged:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationChanged:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
Then in the orientation change event handler and change the transform and the frame. (see my question to see the method which handles the transforms)
CGRect bounds = CGRectMake(0, 0, 100, 100);
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
self.frame = [self getTopRectForBounds:bounds orientation:orientation window:self.window]
CGFloat angle = [self angleForOrientation:orientation];
self.transform = CGAffineTransformMakeRotation(angle);

How to scale (zoom) a UIView to a given CGPoint

I've spent a lot of time trying to find a way to use CGAffineScale to transform a view to a given point, including messing around with anchor points, moving the centre of a view before and after transforming and comprehensive Googling. I am aware this would be a lot simpler with a UIScrollview; but I know it's technically possible to do without one, and it's become a splinter in my mind.
This answer gets remarkably close to what I want to achieve, but the answer only gives details on how to zoom to a given corner (instead of a given point) by cleverly moving the centre to the corner opposite the one you want to zoom in to.
How can I modify mvds' code to scale a UIView to any given point in a UIView?
CGFloat s = 3;
CGAffineTransform tr = CGAffineTransformScale(self.view.transform, s, s);
CGFloat h = self.view.frame.size.height;
CGFloat w = self.view.frame.size.width;
[UIView animateWithDuration:2.5 delay:0 options:0 animations:^{
self.view.transform = tr;
self.view.center = CGPointMake(w-w*s/2,h*s/2);
} completion:^(BOOL finished) {}];
There are 2 steps involved: First you scale up the view you want to zoom in to. Then you set the center of this blown up view such that the part you want to see ends up in the middle of the view.
You should draw this out on paper and the formulas will follow: (untested)
CGFloat s = 3;
CGPoint p = CGPointMake(100, 200);
CGAffineTransform tr = CGAffineTransformScale(self.view.transform, s, s);
CGFloat h = self.view.frame.size.height;
CGFloat w = self.view.frame.size.width;
[UIView animateWithDuration:2.5 delay:0 options:0 animations:^{
self.view.transform = tr;
CGFloat cx = w/2-s*(p.x-w/2);
CGFloat cy = h/2-s*(p.y-h/2);
self.view.center = CGPointMake(cx, cy); //was: (w*s/2,h-h*s/2);
} completion:^(BOOL finished) {}];
I actually ran into this very same problem myself. To fix it, all I did was change the anchor point of the view I was scaling because CGAffineTransforms are performed on the view in relation to its anchor point, so depending on where the anchor point is, the transform will scale, translate, or rotate the view differently. Here's the basic idea:
CGPoint pointToScaleTo = CGPointMake(x, y); //Just enter the coordinates you
//want to scale your view towards
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat viewHeight = self.view.bounds.size.height;
CGFloat scaleFactorX = ...;
CGFloat scaleFactorY = ...;
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scaleFactorX, scaleFactorY);
[UIView animateWithDuration:2.5f delay:0.0f options:0 animations:^{
//I divide the x and y coordinates by the view width and height
//because the anchor point coordinates are normalized to the range
//0.0-1.0.
self.view.layer.anchorPoint = CGPointMake(pointToScaleTo.x/viewWidth, pointToScaleTo.y/viewHeight);
//Now that the anchor point has been changed, apply the scale transform
self.view.layer.transform = scaleTransform;
} completion:^(BOOL finished) {}];

Resources