Disable UITableView scrolling temporarily during drag - ios

I am trying to figure out a way to temporarily (i.e. during a single drag gesture) disable scrolling on a UITableView and then re-enable it to have it pick up where it left off.
My reason is I have a gesture recognizer that is monitoring the drag, and if the user drags their finger above the top of the table, I want to resize the table upwards with their finger, to a point, and then stop resizing and continue scrolling again.
Naturally, I don't want the table to scroll while it is resizing, because that's effectively achieving the scroll itself (by moving the entire table view instead of the inner scrollable content), however I can't figure out how to do this in such a way that it allows the gesture to take effect again after a certain point (or if the user drags back down over the table).
Is there a way to temporarily disable/block a gesture without causing it to fail or cancel outright?
Perhaps I could write a subclass of UITableView that can intercept the gestures and ignore them as needed. What method should I override to do this?
Update:
I ended up approaching this in a different way, which is to simply adjust the contentOffset of the table view at each change of the gesture. I was afraid this might look "jittery" but it actually works quite smoothly. However I'll leave the question open as I'm still curious if this can be done.

At the request of #BrunoGalinari, here is the main part of my implementation of handling pan gestures on a UITableView without breaking the table view's intrinsic scrolling.
tableViewExpanded is a local property that switches between two layout states (expanded or not) and adjusts the bottomViewHeightConstraint constant appropriately. Setting it to itself just readjusts the constraint to one of the two valid values since it is also affected during the pan.
- (void)handlePan:(UIPanGestureRecognizer*)sender {
static CGFloat initialBottomViewY;
static CGFloat initialTableViewContentOffsetY;
static CGFloat initialTouchPointY;
CGPoint touchPoint = [sender locationInView:self.view];
CGFloat splitOffset = touchPoint.y - initialBottomViewY;
BOOL inEffect = ( sender == self.tableViewPan && touchPoint.y < initialBottomViewY ) || ( sender == self.mapViewPan && touchPoint.y > initialBottomViewY );
switch ( sender.state ) {
case UIGestureRecognizerStateBegan: {
initialBottomViewY = self.bottomView.y;
initialTableViewContentOffsetY = self.tableView.contentOffset.y;
initialTouchPointY = touchPoint.y;
break;
}
case UIGestureRecognizerStateEnded: {
self.dragVelocity = [sender velocityInView:self.view].y;
if ( inEffect ) {
if ( ABS( splitOffset ) > 60.f ) { // adjust
if ( sender == self.mapViewPan && touchPoint.y > initialBottomViewY )
self.tableViewExpanded = NO;
else if ( sender == self.tableViewPan && touchPoint.y < initialBottomViewY )
self.tableViewExpanded = YES;
else
self.tableViewExpanded = self.tableViewExpanded;
} else
self.tableViewExpanded = self.tableViewExpanded; // spring back
}
break;
}
case UIGestureRecognizerStateChanged: {
if ( inEffect ) {
self.tableView.contentOffset = CGPointMake( self.tableView.contentOffset.x, initialTableViewContentOffsetY + initialTouchPointY - initialBottomViewY );
self.bottomViewHeightConstraint.constant = self.view.height - touchPoint.y;
self.annotationToSelect = nil;
[self adjustMapAnimated:NO];
}
break;
}
default: {
break;
}
}
}
Here's what the window looks like, to get an idea of placement:

You can disable scrolling of a UITableView by setting:
table.scrollEnabled = NO;
When you are don't with you custom gesture, enable it:
table.scrollEnabled = YES;
It is a property of the parent class UIscrollView:
If the value of this property is YES , scrolling is enabled, and if it is NO , scrolling is disabled. The default is YES.
When scrolling is disabled, the scroll view does not accept touch events; it forwards them up the responder chain.

Related

Interactive transition like Google Maps (iOS)

I want to achieve something very close to what Google Maps (iOS) does and I have some doubts.
But first, a better explanation and things to take into account:
-------------- ANSWER --------------
Thanks to Jugale's contribution, here's a repository so everybody can download and test everything out.
https://github.com/vCrespoP/VCSlidingView
-------------- ORIGINAL QUESTION -----------
You tap in a point inside the map, a view comes in from the bottom but then when you interact with that summary view:
Notice when pulling just a bit, the navigation bar already has set.
When you have scrolled it to the top, you can continue scrolling and the inner scrollview will continue scrolling.
When you 'reverse' the action to dismiss the view, the PanGesture doesn't mess up with the inner scrollView (same for the other way, scrollView VS dismiss)
Here it is in action:
Doubts:
I've tried to do it as an interactive transition (UIPercentDrivenInteractiveTransition) and separating Map from Details in 2 controllers but I'm having troubles with the UIPanGesture interfering with the scrollView.
Maybe it's better to do it as a subview and handle everything there? More or less like MBPullDownController (Although it has some issues with iOS8) -> https://github.com/matej/MBPullDownController
So, anybody knows any framework, has done it, or knows how to do this in a good way?
Thank you for your time :D
Looking through my implementation it seems the following are true:
I have a subclass of UIViewController that is the view controller
I have a subclass of UIView that is the overlay (and henceforth with the known as "the overlay") (actually for me this is a UIScrollView because it needs to go sideways too, but I'll try and filter out the unnecessary code)
I have another subclass of UIView that loads the overlay's content ("the content wrapper")
The content wrapper has a UIScrollView property, in which all other views are loaded ("the content view")
The view controller is responsible for initializing the overlay, setting it's initial frame (where the height is the height of the screen) and passing content to it, nothing more.
From it's -initWithFrame method, the overlay sets itself up with a UIDynamicItemBehavior. It also creates some UICollisionBehavior objects: one at the top of the screen and one below the bottom of the screen at just the right y position for the top of the overlay to be partially visible (as seen in the first frame of your GIF). A UIGravityBehavior is also set up to keep the overlay sitting on the lower collision boundary. Of course, _animator = [[UIDynamicAnimator alloc... is set up too.
Finally:
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan)];
_pan.delegate = self;
_pan.cancelsTouchesInView = FALSE;
The overlay class also has some other helpful methods such as changing the gravity's direction so that the overlay can appear to snap to the top or bottom of the screen.
The _pan handler uses a UISnapBehavior to keep the overlay moving dynamically up and down the screen underneath the user's finger:
- (void)handlePan
{
[self handlePanFromPanGestureRecogniser:_pan];
}
- (void)handlePanFromPanGestureRecogniser:(UIPanGestureRecognizer *)pan
{
CGFloat d = [pan velocityInView:self.superview.superview].y;
CGRect r = self.frame;
r.origin.y = r.origin.y + (d*0.057);
if (r.origin.y < 20)
{
r.origin.y = 20;
}
else if (r.origin.y > [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT)
{
r.origin.y = [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT;
}
if (pan.state == UIGestureRecognizerStateEnded)
{
[self panGestureEnded];
}
else if (pan.state == UIGestureRecognizerStateBegan)
{
[self snapToBottom];
[self removeGestureRecognizer:_tap];
}
else
{
[_animator removeBehavior:_findersnap];
_findersnap = [[UISnapBehavior alloc] initWithItem:self snapToPoint:CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r))];
[_animator addBehavior:_findersnap];
}
}
- (void)panGestureEnded
{
[_animator removeBehavior:_findersnap];
CGPoint vel = [_dynamicSelf linearVelocityForItem:self];
if (fabsf(vel.y) > 250.0)
{
if (vel.y < 0)
{
[self snapToTop];
}
else
{
[self snapToBottom];
}
}
else
{
if (self.frame.origin.y > (self.superview.bounds.size.height/2))
{
[self snapToBottom];
}
else
{
[self snapToTop];
}
}
}
The content wrapper listens for scroll events generated by the content view:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//this is our fancy way of getting the pan to work when the scrollview is in the way
if (scrollView.contentOffset.y <= 0 && _dragging)
{
_shouldForwardScrollEvents = TRUE;
}
if (_shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapper:isForwardingGestureRecogniserTouches:)])
{
[_delegate theContentWrapper:self isForwardingGestureRecogniserTouches:scrollView.panGestureRecognizer];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_dragging = FALSE;//scrollviewdidscroll must not be called after this
if (scrollView.contentOffset.y <= 0 || _shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapperStoppedBeingDragged:)])
{
[_delegate theContentWrapperStoppedBeingDragged:self];
}
}
_shouldForwardScrollEvents = FALSE;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_dragging = TRUE;
}
As you can see, when the bool shouldForwardScrollEvents is TRUE then we send scrollView.panGestureRecognizer to the content wrapper's delegate (the overlay). The overlay implements the delegate methods like so:
- (void)theContentWrapper:(TheContentWrapper *)contentWrapper isForwardingGestureRecogniserTouches:(UIPanGestureRecognizer *)contentViewPan
{
[self handlePanFromPanGestureRecogniser:contentViewPan];
}
- (void)theContentWrapperStoppedBeingDragged:(TheContentWrapper *)contentWrapper
{
//because the scrollview internal pan doesn't tell use when it's state == ENDED
[self panGestureEnded];
}
Hopefully at least some of this is useful to someone!

Continuous vertical scrolling between UICollectionView nested in UIScrollView

While I know nested scrollViews aren't ideal, our designers provided me with this setup, so I'm doing my best to make it work. Let's begin!
View Hierarchy
UIView
UIScrollView (Vertical Scrolling Only)
UIImageView
UICollectionView #1 (Horizontal Scrolling Only)
UIImageView (different from previous UIImageView)
UICollectionView #2 (Vertical Scrolling Only)
Important Note
All my views are defined using programmatic Auto Layout. Each successive view in the UIScrollView's subview hierarchy has a y-coordinate dependency on the view that came before it.
The Problem
For the sake of simplicity, let's modify the nomenclature a bit:
_outerScrollView will refer to UIScrollView
_innerScrollView will refer to UICollectionView #2
I'd like for my _outerScrollView to route its touch event to the _innerScrollView upon reaching the bottom of its contentSize. I'd like the reverse to happen when I scroll back up.
At present, I have the following code:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat bottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);
if (bottomEdge >= [_outerScrollView contentSize].height) {
_outerScrollView.scrollEnabled = NO;
_innerScrollView.scrollEnabled = YES;
} else {
_outerScrollView.scrollEnabled = YES;
_innerScrollView.scrollEnabled = NO;
}
}
where the initial conditions (before any scrolling occurs) is set to:
outerScrollView.scrollEnabled = YES;
innerScrollView.scrollEnabled = NO;
What happens?
Upon touching the view, the outerScrollView scrolls until its bottom edge, and then has a rubber band effect due to _outerScrollView.bounces = YES; If I touch the view again, the innerScrollView scroll until it hits its bottom edge. On the way back up, the same rubber banding effect occurs in the reverse order. What I want to happen is have a fluid motion between the two subviews.
Obviously, this is due to the scrollEnabled conditions that are set in the conditional in the code snippet. What I'm trying to figure out is how to route the speed/velocity of one scrollView to the next scrollView upon hitting an edge.
Any assistance in this issue would be greatly appreciated.
Other Notes
This did not work for me: https://github.com/ole/OLEContainerScrollView
I am considering putting everything in the UIScrollView hierarchy (except for UICollectionView #2) inside UICollectionView #2 supplementaryView. Not sure if that would work.
Figured it out!
First:
_scrollView.pagingEnabled = YES;
Second:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == _scrollView || scrollView == _offersCollectionView) {
CGFloat offersCollectionViewPosition = _offersCollectionView.contentOffset.y;
CGFloat scrollViewBottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);
if (scrollViewBottomEdge >= [_scrollView contentSize].height) {
_scrollView.scrollEnabled = NO;
_offersCollectionView.scrollEnabled = YES;
} else if (offersCollectionViewPosition <= 0.0f && [_offersCollectionView isScrollEnabled]) {
[_scrollView scrollRectToVisible:[_scrollView frame] animated:YES];
_scrollView.scrollEnabled = YES;
_offersCollectionView.scrollEnabled = NO;
}
}
}
Where:
_scrollView is the _outerScrollView
_offersCollectionView is the _innerScrollView (which was UICollectionView #2 in my original post).
Here's what happens now:
When I swipe up (so the view moves down), the offersCollectionView takes over the entire view, moving the other subViews out of the view.
If I swipe down (so the views up), the rest of the subviews come back into focus with the scrollView's bounce effect.
Accepted answer didn't work for me. Here's what did:
Define a subclass of UIScrollView:
class CollaborativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
var lastContentOffset: CGPoint = CGPoint(x: 0, y: 0)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer.view is CollaborativeScrollView
}
}
Since it's not possible to reroute touches to another view, the only way to ensure that the outer scrollview can continue scrolling once the inner one stops is if it had been receiving the touches the whole time. However, in order to prevent the outer one from moving while the inner one is, we have to lock it without setting its isScrollEnabled to false, otherwise it'll stop receiving the touches and won't be able to pick up where the inner one left off when we want to scroll past the inner one's top or bottom.
That's done by assigning a UIScrollViewDelegate to the scrollviews, and implementing scrollViewDidScroll(_:) as shown:
class YourViewController: UIViewController, UIScrollViewDelegate {
private var mLockOuterScrollView = false
#IBOutlet var mOuterScrollView: CollaborativeScrollView!
#IBOutlet var mInnerScrollView: CollaborativeScrollView!
enum Direction {
case none, left, right, up, down
}
func viewDidLoad() {
mOuterScrollView.delegate = self
mInnerScrollView.delegate = self
mInnerScrollView.bounces = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView is CollaborativeScrollView else {return}
let csv = scrollView as! CollaborativeScrollView
//determine direction of scrolling
var directionTemp: Direction?
if csv.lastContentOffset.y > csv.contentOffset.y {
directionTemp = .up
} else if csv.lastContentOffset.y < csv.contentOffset.y {
directionTemp = .down
}
guard let direction = directionTemp else {return}
//lock outer scrollview if necessary
if csv === mInnerScrollView {
let isAlreadyAllTheWayDown = (mInnerScrollView.contentOffset.y + mInnerScrollView.frame.size.height) == mInnerScrollView.contentSize.height
let isAlreadyAllTheWayUp = mInnerScrollView.contentOffset.y == 0
if (direction == .down && isAlreadyAllTheWayDown) || (direction == .up && isAlreadyAllTheWayUp) {
mLockOuterScrollView = false
} else {
mLockOuterScrollView = true
}
} else if mLockOuterScrollView {
mOuterScrollView.contentOffset = mOuterScrollView.lastContentOffset
}
csv.lastContentOffset = csv.contentOffset
}
}
And that's it. This will stop your outer scrollview from scrolling when you begin scrolling the inner one, and get it to pick up again when the inner one is scrolled all the way to one of its ends.

How to do smooth swipe animation in ios

I want to do smooth swipe animation. I just want to that swipe only can be possible when user swipe the page from the right or left border only. Middle of the page swipe should not possible.Both the swipe should be possible left to right and right to left.
I have tried lots of swipe animation sample code or demo code. But its not what I want. I want animation like this https://itunes.apple.com/in/app/clear-tasks-to-do-list/id493136154?mt=8
In this app its like when we touch the right border its swipe smoothly.Please guide me to do this animation. Thanks in advance.
Sorry for the late reply. Just saw this question.
If you want your swipe operation to happen from the edges, create 2 subviews in the far ends (left and right) of your main view and give then a width of 30 or 40.
I believe you have 2 other views popin up from left and right. So inorder to do this you need to add 2 views right on top of your main view.
Now for the left view, set it's right horizondal space constraint connecting to the main view to a value lesser than (-1)x width of the main view. For the right view set its right horizondal space constraint connecting to the main view to a value greater than the width of the main view, so that both the views are outside the main view
X stands for a value greater than or equal to the mainview's width
Add two NSLayoutConstraint variables as IBOutlet holding these 2 values.
NSLayoutConstraint *leftViewHorizondalRightPadding;
NSLayoutConstraint *rightViewHorizondalRightPadding;
Now add the UISwipeGestures to these subViews (indicated in orange).
UISwipeGestureRecognizer *leftToRightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
[leftToRightSwipe setDirection:UISwipeGestureRecognizerDirectionRight];
[self.leftSubview addGestureRecognizer:leftToRightSwipe];
UISwipeGestureRecognizer *rightToLeftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
[rightToLeftSwipe setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.rightSubview addGestureRecognizer:rightToLeftSwipe];
///Now in the swipe handler distinguish the swipe actions
-(void)handleSwipe:(UISwipeGestureRecognizer *)recognizer {
NSLog(#"Swipe received.");
if (recognizer.direction == UISwipeGestureRecognizerDirectionRight) {
//It's leftToRight
leftViewHorizondalRightPadding.constant = 0;
[UIView animateWithDuration:1
animations:^{
[self.view layoutIfNeeded];
}];
}
else {
//It's rightToLeft
rightViewHorizondalRightPadding.constant = 0;
[UIView animateWithDuration:1
animations:^{
[self.view layoutIfNeeded];
}];
}
}
}
This will make a swipe animation from left to right and right to left.
Hope this helps..
After you create the 2 swipe gesture recognisers you should set their delegates. Then use this delegate method:
UISwipeGestureRecognizer *_swipeLeft;
UISwipeGestureRecognizer *_swipeRight;
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
static const CGFloat borderWidth = 50.0f;
if(gestureRecognizer == _swipeLeft) {
return [gestureRecognizer locationInView:self].x > self.frame.size.width - borderWidth;
}
else if(gestureRecognizer == _swipeRight) {
return [gestureRecognizer locationInView:self].x < borderWidth;
}
return YES;
}
Do note that for smooth swiping/dragging you will probably need to use a pan gesture or even long press gesture recogniser rather then the swipe gesture. They are very similar except the long press takes a bit of time to begin (which is settable). If you use them you may still want to use the same delegate method. Or you can simply do all the code in the gestures target method. Try something like this:
CGPoint gestureStartPoint;
- (void)dragFromBoreder:(UIGestureRecognizer *)sender {
static const CGFloat borderWidth = 50.0f;
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
CGPoint location = [sender locationInView:self];
if(location.x > borderWidth || location.x < self.frame.size.width-borderWidth) {
//break the gesture
sender.enabled = NO;
sender.enabled = YES;
}
else {
gestureStartPoint = location;
}
break;
}
case UIGestureRecognizerStateChanged: {
CGPoint location = [sender locationInView:self];
CGFloat deltaX = location.x - gestureStartPoint.x;
UIView *viewToMove;
CGPoint defaultCenter;
viewToMove.center = CGPointMake(defaultCenter.x+deltaX, defaultCenter.y);
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
CGPoint location = [sender locationInView:self];
CGFloat deltaX = location.x - gestureStartPoint.x;
/*
if(deltaX > someWidth) {
show the left view
}
else if(deltaX < -someWidth) {
show the right view
}
else {
put everything back the way it was
}
*/
break;
}
default:
break;
}
}
In ios7 there is a gesture recogniser specifically for gestures beginning from the edge of the screen. You should use this.
I can't help with your "smooth" problem, because you haven't said what your current animation looks like or how you are doing it. But a pan gesture, like the one linked, which directly updates view positions, will track the user's movement much more smoothly than a swipe.

UITableView Subview when swipe to show "extras"

I am trying to implement a "extras" menu for a app I am creating. Essentially I have a UITableView with multiple types of cells, when a user swipes to the left on the cell I want to be able to dynamically show them "extras" they can do. For example on a post they can swipe over and then see options like share, like ...ect. If you have used the Alien Blue app on ios for Reddit then that is what I am looking to do...
So far I have the swipe recognizer working and it identifies the type of cell properly... I just don't know how to start the subview programming...
Do I just make every cell larger and hide the extras until swipe or do I dynamically add views to each cell as I go...
Thank for any advice or help
I can provide code if needed....
A
I think you are right, and at least, that's how I did on my cell.
The only difference is i am not making the cell width large than window width. I add a view with the extra stuff, in my case it's a delete button, under the regular cell.contentview. then when the cell detects a swipe from right to left, it will call a function to handle the gesture.
Since i want the user to see that delete button when drag the cell to the left and show that button entirely when they pan the cell far enough
here is a snippet for how i handle the pan gesture,
CGPoint _originalTouchPoint;
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
{
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
//save the original touch point when pan starts
_originalTouchPoint = [recognizer locationInView:_tableView];
break;
case UIGestureRecognizerStateChanged:
[self handlePan:recognizer];
break;
case UIGestureRecognizerStateEnded:
[self panEnded:recognizer];
break;
default:
break;
}
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
{
MyCell *ourCell = (MyCell *)recognizer.view;
CGPoint touchPoint = [recognizer locationInView:_tableView];
float movedDistance = (_originalTouchPoint.x - touchPoint.x);
if(movedDistance < 0)
movedDistance = 0;
float maxX = ourCell.deleteButton.frame.size.width;
float ourTranslation = MAX(-1 * ourCell.deleteButton.frame.size.width, -1 * movedDistance);
ourCell.contentView.transform = CGAffineTransformMakeTranslation(ourTranslation, 0.0f);
// i only show the button when user pan all the way though
_shouldDeleteButtonShow = movedDistance / maxX >= 1.0;
}
- (void)panEnded:(UIPanGestureRecognizer *)recognizer
{
MyCell *ourCell = (MyCell *)recognizer.view;
if (_shouldDeleteButtonShow)
{
//do whatever you want in this case
}
else
{
//move the cell back to normal
[UIView animateWithDuration:0.3
animations:^
{
ourCell.contentView.transform = CGAffineTransformIdentity;
}];
}
}
Those are my codes, maybe not working exactly like you want, but hopefully, that gave you a rough idea about how to do this

iOS >> Dragged View is Jumping Back to Original Position >> Auto Layout Combined with UIPanGestureRecognizer Issue

I have several UIViews that the user should be able to drag & drop. I use UIPanGestureRecognizer to manage the d&d action (see code below). The d&d action is working fine, but when I start dragging another UIView, the one that I just dropped is jumping back to its original position.
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
for (UIView* checkView in dragAndDropImageViewsArray) {
if (checkView == recognizer.view) {
[checkView.superview bringSubviewToFront:checkView]; //this is done to make sure the currently dragged object is the top one
}
}
break;
case UIGestureRecognizerStateChanged:
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y);
break;
case UIGestureRecognizerStateEnded:
NSLog(#"UIGestureRecognizerStateEnded");
break;
default:
break;
}
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
}
I tried several things that "sloved" the problem but caused other problems:
Cancel Auto-Layout: That totally solves it but now I have to start calculate and manage object locations and sizes for different devices and orientations.
Use layer.zPosition instead of bringSubviewToFront: that makes objects appear above other objects while apparently avoiding the "jump back", but does not allow me to change the hierarchy between the different draggable objects.
I started deepening my understanding about Auto Layout, but it gets very complicated when it seems to me that what I'm looking for should be very simple; I saw many methods that deal with performing a "re-auto-layouting", but I'm not clear about how to work with them.
Is there a simple solution here? like calling a method that overrides the IB Auto-Layout constrains and redefine them according to the UIView new position?
Any assistance would be very musch appreciated.
I think that if you are using autolayout and you want to change the position of a view, you need to be updating the constraints on the view instead of manually setting the center of a view. From the description it sounds like you are setting the center, but when the app next lays out the views (eg you drag another) it re-sets the center to whatever autolayout says it should be.
I'm not sure what the rest of your app looks like, but you may be able to find nearby views and add constraints to it, or you might prefer to just update the constraints of the dragged view and set the leading space and top space to the superview manually. If you've got a collection handy you could also clear all the constraints and then re-add them as you'd like to keep padding in between the views.
To set the constraints in code you can clear the constraints for the moved view and re-add them, or if you have a view in IB you can drag the constraints into code to create an outlet. Look at the removeConstraints and addConstraints methods to do it in code.
Lastly, if it works how you'd like without autolayout, you could try setting translatesAutoresizingMaskIntoConstraints = YES on the moved views, but be aware that may cause conflicts elsewhere.
This is all possible using Interface Builder. Here is the code I used to do this:
#property (weak,nonatomic) IBOutlet NSLayoutConstraint *buttonXConstraint;
#property (weak,nonatomic) IBOutlet NSLayoutConstraint *buttonYConstraint;
Then hook up those IBOutlets to the Horizontal Constraint(X Position) and Vertical Constraint(Y Position) in the Interface Builder. Make sure that the Constraints are hooked up to the base view and not it's closest view.
Hook up a pan gesture and then use this following code to drag your object around:
- (IBAction)panPlayButton:(UIPanGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateBegan){
} else if(sender.state == UIGestureRecognizerStateChanged){
CGPoint translation = [sender translationInView:self.view];
//Update the constraint's constant
self.buttonXConstraint.constant += translation.x;
self.buttonYConstraint.constant += translation.y;
// Assign the frame's position only for checking it's fully on the screen
CGRect recognizerFrame = sender.view.frame;
recognizerFrame.origin.x = self.buttonXConstraint.constant;
recognizerFrame.origin.y = self.buttonYConstraint.constant;
// Check if UIImageView is completely inside its superView
if(!CGRectContainsRect(self.view.bounds, recognizerFrame)) {
if (self.buttonYConstraint.constant < CGRectGetMinY(self.view.bounds)) {
self.buttonYConstraint.constant = 0;
} else if (self.buttonYConstraint.constant + CGRectGetHeight(recognizerFrame) > CGRectGetHeight(self.view.bounds)) {
self.buttonYConstraint.constant = CGRectGetHeight(self.view.bounds) - CGRectGetHeight(recognizerFrame);
}
if (self.buttonXConstraint.constant < CGRectGetMinX(self.view.bounds)) {
self.buttonXConstraint.constant = 0;
} else if (self.buttonXConstraint.constant + CGRectGetWidth(recognizerFrame) > CGRectGetWidth(self.view.bounds)) {
self.buttonXConstraint.constant = CGRectGetWidth(self.view.bounds) - CGRectGetWidth(recognizerFrame);
}
}
//Layout the View
[self.view layoutIfNeeded];
} else if(sender.state == UIGestureRecognizerStateEnded){
}
[sender setTranslation:CGPointMake(0, 0) inView:self.view];
}
I have added code in there to check the frame and make sure that it's not going outside the view. If you want to allow the object to be partially off the screen, feel free to take it out.
This took a little bit of time for me to realize that it was the use of AutoLayout that was resetting the view but constraints will do what you need here!

Resources