Dynamically change view constraint when related view is dragged - ios

I have two views that overlay each other. When i drag viewA upward, viewB becomes visible. When viewAis dragged all the way to the top, these two views are lined up perfectly so that viewA.bottom == viewB.top. This is accomplished by using autolayout/constraints and works really nice on 4" screen, but the alignment breaks for 4,7" and 5,5" screens.
I need to adjust the height or the top-space-constraint on viewB at runtime so it matches the viewA.bottom value.
So far I've been playing around with KVO for listening on viewB.frame changes (smelly) and manipulated the scrollview frame directly, but without luck. The following code snippet is the most promising, but I can't make it to work in "realtime". recognizer.view is the infoCenterView.
UIView *infoCenterView; // viewA
UIScrollView *scrollView; // viewB
UITableView *tableView; // child of viewB
NSLayoutConstraint *scrollViewTopSpaceConstraint;
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint center = recognizer.view.center;
CGPoint translation = [recognizer translationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateChanged) {
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
center.y += translation.y;
// Don't allow translation outside screen frame
if (center.y < self.yMin || center.y > self.yMax)
return;
recognizer.view.center = center; // (1)
self.scrollViewTopSpaceConstraint.constant += translation.y; // (2)
}
else if (recognizer.state == UIGestureRecognizerStateEnded) {
if (center.y > self.yMin && center.y < ((self.yMax + self.yMin) / 2)) {
center.y = self.yMin; // Move to top
}
else if (center.y >= ((self.yMax + self.yMin) / 2) && center.y < self.yMax) {
center.y = self.yMax; // Move to bottom
}
[UIView animateWithDuration:0.2
delay:0
options:UIViewAnimationOptionTransitionNone
animations:^{
recognizer.view.center = center;
}
completion:^(BOOL finished){
// (3)
self.scrollViewTopSpaceConstraint.constant = 200;
}];
}
// (1) and (2)
If both of these lines is executed, only the tableView height is adjusted in "realtime" and infoCenterView will not move. However, if (2) is commented out, infoCenterView can be moved.
// (3)
If (2) is commented out and (3) is executed, and if infoCenterView is dragged to the top of the screen, it will unintentionally drop to the bottom and tableView height is properly set.
What is going on here? Ideas are very welcome!

Related

Pang Gesture Recognizer stop

want to use a UIView like a UIScrollView for more reason, i create a Action with PanGestureRecognizer that's the simple code:
- (IBAction)moveUp:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.containerView];
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
But i want to stop dragging when my containerView bottom is in exact position, follow the screens:
This is the full viewController, all button you see is inside a containerView
This is when i open the application:
The header image is in a ViewController below and all button you see is inside a containerView, now i want to fix this position with absolute y, my containerView can't drag down.
Now the function can't stop, when i dragUp this is what happen:
My containerView can drag over the end of the containerView and i don't want that is possible, i want block the drag up in the end of containerView like this screen:
And when i drag down again i want to stop drag in starter position (screen 1).
Thanks for any idea.
What you are trying to achieve sounded fun to me at first glance, so I wanted to give it a try.
I modified your gesture recognizer's call back to the following:
- (IBAction)panned:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:yourContainerView];
CGFloat yWithinBounds = 0.0f;
if(recognizer.view.center.y + (recognizer.view.bounds.size.height / 2.0f) > yourContainerView.bounds.size.height)
{
yWithinBounds = yourContainerView.bounds.size.height - (recognizer.view.bounds.size.height / 2.0f);
[UIView animateWithDuration:1.0f animations:^
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, yWithinBounds);
}];
}
else if(recognizer.view.center.y - (recognizer.view.bounds.size.height / 2.0f) < yourContainerView.bounds.origin.y)
{
yWithinBounds = yourContainerView.bounds.origin.y + (recognizer.view.bounds.size.height / 2.0f);
[UIView animateWithDuration:1.0f animations:^
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, yWithinBounds);
}];
}
else
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0.0f, 0.0f) inView:self.view];
}
}
In the first if, we are checking if the current center point of your contained view + half the size of it is going out of bounds of the containing view on the bottom; if it is, instead of simply disallowing it to go further, I took the liberty of putting this in an animation block to animate the contained view back to within the containing view so that it looks a bit better visually.
In the else-if, we are doing the same except this time for the top of the containing view - we are checking if the contained view is going out of bounds on top; if it is, we animate it back within the containing view.
If the else clause is reached, that would mean the contained view is within the containing view's bounds - here, we simply translate it like you originally do.
Here is how I set up the views as test:
yourContainerView = [[UIView alloc] initWithFrame:self.view.bounds];
yourContainerView.backgroundColor = [UIColor brownColor];
[self.view addSubview:yourContainerView];
UIView *viewWithinContainer = [[UIView alloc] initWithFrame:(CGRect){{yourContainerView.bounds.origin.x, yourContainerView.bounds.origin.y}, yourContainerView.bounds.size.width, 50.0f}];
viewWithinContainer.backgroundColor = [UIColor yellowColor];
[yourContainerView addSubview:viewWithinContainer];
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panned:)];
[viewWithinContainer addGestureRecognizer:panGesture];
Feel free to adjust the height (i.e., 50.0f to other values) of the view (hereby: the viewWithinContainer) within the container; the way the code is set up, you would not need to change any other values else where even if you change the height.
You could also change the animation duration to your need.
I am moving the thing up and down in my simulator at the moment; looks simple but fun to play around.
Update after question update:
Your new update seems to differ somewhat greatly from what you originally asked, but here is the solution:
The complexity is direction. There are other ways to find out; but I am using mine here, through which we don't need to create another variable.
If you are using storyboard, please set up an IBOutlet for your container view, because we would like to obtain the following values (carry out the following in initWithFrame and initWithCoder):
p1 = viewWithinContainer.center.y;
p2 = viewWithinContainer.center.y - (yourContainerView.bounds.size.height - viewWithinContainer.bounds.size.height);
Please set up p1 and p2 as ivars:
CGFloat p1;
CGFloat p2;
Here is the updated method for a contained view larger than the containing view:
- (IBAction)panned:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:yourContainerView];
if(translation.y < 0)
{
if(recognizer.view.center.y + (recognizer.view.bounds.size.height / 2.0f) + translation.y > yourContainerView.bounds.size.height) {
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
else
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, p2);
[recognizer setTranslation:translation inView:self.view];
}
}
else if(translation.y > 0)
{
if(recognizer.view.center.y - (recognizer.view.bounds.size.height / 2.0f) + translation.y < yourContainerView.bounds.origin.y)
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
else
{
recognizer.view.center = CGPointMake(recognizer.view.center.x, p1);
[recognizer setTranslation:translation inView:self.view];
}
}
}
The method will ensure that, as you scroll down, the top of the contained view will stop at the edge of the container on top; and as you scroll up, at the edge of the container on the bottom.
I ran this in the simulator; and japp, it has been fun doing this. Enjoy. :-)

iOS UIView animation when table view scroll?

I want animation like home screen of this app.This screen has a view (with a label on it) on top and a table view under it. When i scroll the table the top view and title on it become small with animation and after a specific point it becomes like navigation bar with title in centre. And the same thing when i scroll down.
Here is what i tried so far
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
NSLog(#"y is::: %f", offset.y);
if (offset.y > 0 && offset.y < 16 && self.topView.frame.size.height > 64) {
[UIView animateWithDuration:0.8 animations:^{
CGRect rect = self.topView.frame;
rect.size.height = rect.size.height-offset.y;
self.topView.frame = rect;
}completion:^(BOOL finish){
}];
}
else if(offset.y <= 0 && offset.y > -16 && self.topView.frame.size.height < 80)
{
[UIView animateWithDuration:0.8 animations:^{
CGRect rect = self.topView.frame;
rect.size.height = 80;
self.topView.frame = rect;
}completion:^(BOOL finish){
}];
}
}
Note that my top view initial height is 80 and i want to make it 64 when scroll up and vice versa.
with this code the view height animated but it depends upon animation duration.So if i scroll fast then it is not working properly.The original app i mentioned have very smooth animation. How could i achieve that?
Screen shot of original App-
These screen shots showing three different scroll position, please note the "MySurgery" text on top.1- Larger text and right align.
2- small text and near centre position
3- smaller text and centralised(like navigation bar)
I think you dont need animation for this, you just need some calculations and setting frame of the top view. like,
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
if (offset.y>0 && offset.y<64) {
[topView layoutIfNeeded];
CGRect viewFrame = topView.frame;
viewFrame.size.height--;
topView.frame = viewFrame;
}
}
Note that layoutIfNeeded is required if your view is using autolayout

Limiting my UIPanGestureRecognizer

I have the following code and it works just fine, but I can't really explain it to myself. My goal was to drag my UIView from the bottom and have it stop moving when the bottom of the UIView reaches the center of the screen. The code does just that, but I'm still confused by how the limit i set (newCenter.y >= 0 && newCenter.y <= 284) works.
My perception of whats happening is that when I begin dragging the UIView from the very bottom towards the top of the screen, my newCenter.y is constantly changing (decreasing) as I drag upwards. But what makes it stop when the bottom of the view being dragged reaches the center of the screen? The view stops dragging way before I even get close to 0.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(self.view.bounds.size.width / 2,
recognizer.view.center.y + translation.y);
if (newCenter.y >= 0 && newCenter.y <= 284) {
recognizer.view.center = newCenter;
[recognizer setTranslation:CGPointZero inView:self.view];
}
}

How to restrict a moveable view by Pan gesture

I have a UIImageView which is moveable via a pan gesture.
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.photoMask addGestureRecognizer:pan];
I would like to restrict the area this can be moved on screen. Rather than the user be able to drag the view right to the side of the screen, I want to restrict it by a margin of some sort. How can I do this?
Also, how is this then handled when rotated?
EDIT ---
#pragma mark - Gesture Recognizer
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
NSLog(#"Pan Gesture");
gesture.view.center = [gesture locationInView:self.view];
}
This is my current method to handle the pan. What I need to do is continue to move the imageview by the center point and also restrict its movement when close to the edge of the screen by 50 for example.
One possible solution to this is in your handlePan method, check the location of the point on the screen, and only commit the change if it is within the bounds you wish to restrict it to.
For ex.
-(void) handlePan:(UIGestureRecognizer*)panGes{
CGPoint point = [panGes locationInView:self.view];
//Only allow movement up to within 100 pixels of the right bound of the screen
if (point.x < [UIScreen mainScreen].bounds.size.width - 100) {
CGRect newframe = CGRectMake(point.x, point.y, theImageView.frame.size.width, theImageView.frame.size.height);
theImageView.frame = newframe;
}
}
I believe this would also correctly handle any screen rotation
EDIT
To move your image view by the center of its frame, the handlePan method could look something like this.
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
//Only allow movement up to within 50 pixels of the bounds of the screen
//Ex. (IPhone 5)
CGRect boundsRect = CGRectMake(50, 50, 220, 448);
if (CGRectContainsPoint(boundsRect, point)) {
imgView.center = point;
}
}
Check whether the point is within your desired bounds, and if so, set the center of your image view frame to that point.
I'm not sure if I'm being over-simplistic here but I think you can accomplish this by using an if clause.
-(void)handlePan:(UIPanGestureRecognizer*)gesture {
UIImageView *viewToDrag = gesture.view; // this is the view you want to move
CGPoint translation = [gesture translationInView:viewToDrag.superview]; // get the movement delta
CGRect movedFrame = CGRectOffset(viewToDrag.frame, translation.x, translation.y); // this is the new (moved) frame
// Now this is the critical part because I don't know if your "margin"
// is a CGRect or maybe some int values, the important thing here is
// to compare if the "movedFrame" values are in the allowed movement area
// Assuming that your margin is a CGRect you could do the following:
if (CGRectContainsRect(yourPermissibleMargin, movedFrame)) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
// -OR-
// If you have your margins as int values you could do the following:
if ( (movedFrame.origin.x + movedFrame.size.width) < 50) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
}
You'll probably have to adapt this to meet your specific needs.
Hope this helps!
Here is the answer in Swift 4 -
Restrict the view's movement to superview
#objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer)
{
// Allows smooth movement of stickers.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed
{
let point = gestureRecognizer.location(in: self.superview)
if let superview = self.superview
{
let restrictByPoint : CGFloat = 30.0
let superBounds = CGRect(x: superview.bounds.origin.x + restrictByPoint, y: superview.bounds.origin.y + restrictByPoint, width: superview.bounds.size.width - 2*restrictByPoint, height: superview.bounds.size.height - 2*restrictByPoint)
if (superBounds.contains(point))
{
let translation = gestureRecognizer.translation(in: self.superview)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.superview)
}
}
}
}
If you want more control over it, match restrictByPoint value to your movable view's frame.
- (void)dragAction:(UIPanGestureRecognizer *)gesture{
UILabel *label = (UILabel *)gesture.view;
CGPoint translation = [gesture translationInView:label];
if (CGRectContainsPoint(label.frame, [gesture locationInView:label] )) {
label.center = CGPointMake(label.center.x,
label.center.y);
[gesture setTranslation:CGPointZero inView:label];
}
else{
label.center = CGPointMake(label.center.x,
label.center.y + translation.y);
[gesture setTranslation:CGPointZero inView:label];
}
}

UIPanGestureRecognizer to maximize and minimize a UIView

To minimize and maximize a UIView I am using a UIPangestureRecognizer. The code is listed below:
-(void) pannningMyView:(UIPanGestureRecognizer*) panGesture{
CGPoint newPoint=[panGesture translationInView:self.view];
CGPoint oldPoint=self.myPanningView.frame.origin;
CGFloat dx=newPoint.x;
CGFloat dy=newPoint.y;
if(dx>0){
CGRect oldRect=self.myPanningView.frame;
oldRect.size.width+=dx;
oldRect.size.height+=dy;
self.myPanningView.frame=oldRect;
}
}
But, the transition is very fast, such that I move few pixels and it covers the entire screen. I am not able to figure out what correction is required to my code.
The problem is that your translations are cumulative because the translationInView is from the beginning of the continuous gesture, but you're you're adding the translation to the current frame, not the original frame. This is solved by checking the gesture state, and if you're at the start of the gesture then save the original frame, and then use that as the basis for future translations as the gesture proceeds.
-(void) panningMyView:(UIPanGestureRecognizer*) panGesture
{
static CGRect originalFrame; // or you could make this a non-static class ivar
if (panGesture.state == UIGestureRecognizerStateBegan)
{
originalFrame = self.myPanningView.frame;
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [panGesture translationInView:self.view];
if (translation.x > 0) {
CGRect newFrame = originalFrame;
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
self.myPanningView.frame = newFrame;
}
}
}
Note, I got rid of oldPoint because you didn't seem to be using it. I also renamed newPoint to translation because it's not a point on the screen but a measure of how much your finger has moved (or translated) on the screen. I also renamed oldRect to newFrame, because I think that more accurately captures what it is.
Essentially, I've tried to preserve the logic of your routine, but simply clarify your logic and variable names. I would have thought that you might want an additional else if cause, checking for ended or canceled gestures, using an animation to complete or reverse the gesture as appropriate, but I didn't tackle that as you didn't reference this in your original question.
Regardless, I hope you get the idea of what we're doing here. We're saving the original frame and applying the translation to that rather than applying it to the current frame.
Update:
In a follow up question, you asked how you might clarify the animation. You might do something like:
-(void) panningMyView:(UIPanGestureRecognizer*) panGesture
{
static CGRect originalFrame; // or you could make this a non-static class ivar
CGPoint translation = [panGesture translationInView:self.view];
if (panGesture.state == UIGestureRecognizerStateBegan)
{
originalFrame = self.myPanningView.frame;
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
if (translation.x > 0) {
CGRect newFrame = originalFrame;
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
self.myPanningView.frame = newFrame;
}
}
else if (panGesture.state == UIGestureRecognizerStateEnded ||
panGesture.state == UIGestureRecognizerStateCancelled ||
panGesture.state == UIGestureRecognizerStateFailed)
{
CGRect finalFrame = originalFrame;
// if we've gone more than half way, move it all the way,
// otherwise return it to the original frame
if (translation.x > (self.view.frame.size.width / 2.0))
{
finalFrame.size.width += self.view.frame.size.width;
finalFrame.size.height += self.view.frame.size.height;
}
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.myPanningView.frame = finalFrame;
}
completion:nil];
}
}

Resources