I have a collectionViewController with horizontal collectionView like in Paper App. I added pan gesture to change from one layout to other and I used interactive transition. It works well if you drag and wait when animation is finished, but if you drag faster several times and don't wait animation to be finished or canceled app throw an exception:
Assertion failure in -[UICollectionView _finishInteractiveTransitionShouldFinish:finalAnimation:], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:2691
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'the collection was not prepared for an interactive transition. see startInteractiveTransitionToCollectionViewLayout:completion:'
Gesture handler code :
- (void)oneFingerGesture:(UIPanGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded ||
sender.state == UIGestureRecognizerStateCancelled)
{
if (self.transitionLayout.transitionProgress > 0.2) {
[self.collectionView finishInteractiveTransition];
} else {
[self.collectionView cancelInteractiveTransition];
}
}else
{
CGPoint point = [sender locationInView:sender.view];
if (sender.state == UIGestureRecognizerStateBegan && !self.transitionLayout && !_isInTransition)
{
invertPan = self.largeLayout == self.collectionView.collectionViewLayout;
UICollectionViewLayout *toLayout = invertPan ? self.smallLayout : self.largeLayout;
self.transitionLayout = [self.collectionView startInteractiveTransitionToCollectionViewLayout:toLayout
completion:^(BOOL completed, BOOL finish) {
self.transitionLayout = nil;
_isInTransition = NO; }];
self.initialTapPoint = point;
_isInTransition = YES;
}else if(sender.state == UIGestureRecognizerStateChanged && self.transitionLayout && _isInTransition)
{
CGFloat distance = _initialTapPoint.y - point.y;
if (invertPan) {
distance = -distance;
}
CGFloat dimension = self.collectionView.bounds.size.height - 200;
CGFloat progress = MAX(MIN(((distance)/ dimension), 1.0), 0.0);
[self.transitionLayout setTransitionProgress:progress];
}
}
}
The documentation states that calls to finishInteractiveTransition: and cancelInteractiveTransition: will install the layout object the transition is going to and from respectively. However, this appears to not happen immediately. Thus, if the gesture triggering and driving the transition can happen in quick succession, it is not sufficient to check whether the current layout is a UICollectionViewTransitionLayout or a subclass thereof before calling one of the two methods to end the transition.
I solved the problem by introducing a BOOL ivar (_isFinishingOrCancellingTransition) to avoid ending a transition that is already in the process of ending:
if (_isFinishingOrCancellingTransition) return;
if (!self.transitionLayout) return;
if (self.collectionView.collectionViewLayout != self.transitionLayout) return;
_isFinishingOrCancellingTransition = YES;
if (self.transitionLayout.transitionProgress > 0.5)
{
[self.collectionView finishInteractiveTransition];
}
else
{
[self.collectionView cancelInteractiveTransition];
}
and then resetting the BOOL when the transition is completed:
self.transitionLayout = [self.collectionView startInteractiveTransitionToCollectionViewLayout:toLayout completion:^(BOOL completed, BOOL finish) {
self.transitionLayout = nil;
_isFinishingOrCancellingTransition = NO;
}];
Related
I'd like to be able to have a UIView (or, if necessary, UIViewController), which can be 'dragged'/flicked/swiped off screen - but locked to the vertical axis (so the view would go up or down off the screen to show the view below). This is sort of like the Facebook app's web views.
Could this perhaps be achieved with UIGravityBehavior?
Any ideas would be fantastic.
I have made something similar a while ago by using a UIPanGestureRecognizer. Here is how this would work:
float slidingMenuOffsetSum;
- (void)handleSlidingMenuPan:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:_slidingMenuView];
slidingMenuOffsetSum += translation.y;
CGPoint newSlidingMenuCenter = CGPointMake(_slidingMenuView.center.x, _slidingMenuView.center.y + translation.y);
if (_slidingMenuView.frame.origin.y <= 512 && _slidingMenuView.frame.origin.y >= 0) {
_slidingMenuView.center = newSlidingMenuCenter;
}
[gestureRecognizer setTranslation:CGPointZero inView:_slidingMenuView];
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
[UIView animateWithDuration:0.3
animations:^{
[_slidingMenuView setUserInteractionEnabled:false];
if (slidingMenuOffsetSum > 0) {
if (slidingMenuOffsetSum > _slidingMenuView.frame.size.height / 4) {
[self setSlidingMenuHidden:false];
} else {
[self returnSlidingMenuToPosition];
}
} else {
if (slidingMenuOffsetSum < -_slidingMenuView.frame.size.height / 4) {
[self setSlidingMenuHidden:true];
} else {
[self returnSlidingMenuToPosition];
}
}
}
completion:^(BOOL finished) {
slidingMenuOffsetSum = 0;
[_slidingMenuView setUserInteractionEnabled:true];
}];
}
}
You can adjust the values at which the sliding menu can move, and the thresholds for detecting if the view should go back to its original position, or eg. go off the screen.
I am having a problem while trying to make a custom "sliding" object from scratch. I have a view self.taskView which should either stick to the left or stick to the right. This is working fine until I try to change the state of an instance variable called self.task.
This is a (webm) animation of it working "correctly": working.webm
But this only works when I do NOT change the instance variable self.task.state
This is a (webm) animation of it NOT working "correctly": not_working.webm
So I know that it is the code that changes the state of the task that is bugging it because if I just comment out all the [self setState:BOOL] statements it works as seen in "working.webm".
Any tips or ideas are appreciated. But I am a novice so please keep it simple if it is possible.
// Code to move the green UIview right or left
-(IBAction)handlePan:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
self.origin = CGPointMake(self.taskView.frame.origin.x, self.taskView.frame.origin.y);
} else if (sender.state == UIGestureRecognizerStateChanged) {
CGPoint touchLocation = [sender locationInView:self];
CGPoint location = CGPointMake(touchLocation.x, (self.taskView.frame.size.height/2));
self.taskView.center = location;
} else if (sender.state == UIGestureRecognizerStateEnded) {
if (self.origin.x == 0) {
// Stick right
if (self.taskView.center.x > ((self.frame.size.width/10)*4)) {
[self setPosition:NO]; // This sets the position of the view
[self setState:NO]; // This code is bugging the "animation"
} else { // Else stick to the left
[self setPosition:YES];
[self setState:YES];
}
} else {
// Stick left
if (self.taskView.center.x < ((self.frame.size.width/10)*6)) {
[self setPosition:YES];
[self setState:YES];
} else { // Else stick right
[self setPosition:NO];
[self setState:NO];
}
}
/*[UIView animateWithDuration:0.2
delay:0.01
options:UIViewAnimationOptionAllowUserInteraction
animations:^
{
[self.taskView setFrame:position];
} completion:^(BOOL finished){
}];*/
} else if (sender.state == UIGestureRecognizerStateCancelled) {
}
}
// THIS METHOD IS BUGGING THE "ANIMATION"
- (void)setState:(BOOL)state {
self.task.state = [NSNumber numberWithBool:state];
}
I think the problem is that you're trying to run a time-consuming method on the main thread, and that's why your animations are choppy.
Your problem as you mentioned resides here:
- (void)setState:(BOOL)state {
self.task.state = [NSNumber numberWithBool:state];
}
This is a method that probably does something more than just changing a property (to my understanding from your comments this is a database object)
What you could do, is to update this object on a background thread:
- (void)setState:(BOOL)state {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
self.task.state = [NSNumber numberWithBool:state];
});
}
I've completely implemented the UILongGesture in my App which exchanges the cell value by drag and drop. For now I've requirement that if I move first row with last row then first row should remain at first position means don't want change the position.
I've tried chunk of codes and wasted my time but couldn't get result. Below is my code.
- (IBAction)longPressGestureRecognized:(id)sender{
UILongPressGestureRecognizer *longGesture = (UILongPressGestureRecognizer *)sender;
UIGestureRecognizerState state = longGesture.state;
CGPoint location = [longGesture locationInView:self.tblTableView];
NSIndexPath *indexpath = [self.tblTableView indexPathForRowAtPoint:location];
static UIView *snapshotView = nil;
static NSIndexPath *sourceIndexPath = nil;
switch (state) {
case UIGestureRecognizerStateBegan:
if (indexpath) {
sourceIndexPath = indexpath;
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:indexpath];
snapshotView = [self customSnapshotFromView:cell];
__block CGPoint center = cell.center;
snapshotView.center = center;
snapshotView.alpha = 0.0;
[self.tblTableView addSubview:snapshotView];
[UIView animateWithDuration:0.25 animations:^{
center.y = location.y;
snapshotView.center = center;
snapshotView.transform = CGAffineTransformMakeScale(1.05, 1.05);
snapshotView.alpha = 0.98;
cell.alpha = 0.0;
} completion:^(BOOL finished) {
cell.hidden = YES;
}];
}
break;
case UIGestureRecognizerStateChanged: {
CGPoint center = snapshotView.center;
center.y = location.y;
snapshotView.center = center;
if (indexpath && ![NSIndexPath isEqual:sourceIndexPath]) {
[self.namesArray exchangeObjectAtIndex:indexpath.row withObjectAtIndex:sourceIndexPath.row];
[self.tblTableView moveRowAtIndexPath:sourceIndexPath toIndexPath:indexpath];
sourceIndexPath = indexpath;
NSIndexPath *indexPathOfLastItem =[NSIndexPath indexPathForRow:([self.namesArray count] - 1) inSection:0];
NSLog(#"last :::: %#",indexPathOfLastItem);
if (indexpath==indexPathOfLastItem) {
[self.namesArray exchangeObjectAtIndex:indexPathOfLastItem.row withObjectAtIndex:sourceIndexPath.row];
[self.tblTableView moveRowAtIndexPath:indexPathOfLastItem toIndexPath:0];
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
}
}
break;
}
default: {
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
[UIView animateWithDuration:0.25 animations:^{
snapshotView.center = cell.center;
snapshotView.transform = CGAffineTransformIdentity;
snapshotView.alpha = 0.0;
cell.alpha = 1.0;
} completion:^(BOOL finished) {
sourceIndexPath = nil;
[snapshotView removeFromSuperview];
snapshotView = nil;
}];
break;
}
}
}
EDIT: What I have come across is the cell is not exchanging that's what I want but it is hidden. Here is the image: Image1 and Image2
First of all, I don't think you should do the row exchanges in UIGestureRecognizerStateChanged but instead in UIGestureRecognizerStateEnded. UIGestureRecognizerStateChanged is processed rapidly(many many times) while you are long pressing and moving your finger across the screen. So this causes the row exchange code to run many times which is not your intention I guess. UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded are run once per each longpress.
I would keep these following three lines of code in UIGestureRecognizerStateChanged and move the rest to UIGestureRecognizerStateEnded:
CGPoint center = snapshotView.center;
center.y = location.y;
snapshotView.center = center;
The UIGestureRecognizerStates are as follows:
UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
And instead of using switchs default:, I believe it will be better to cover more of these states in your state machine logic.
When your finger detaches from the screen during animation, iOS returns gesture.state = UIGestureRecognizerStatePossible.
So remember to handle that gesture!
I used MCPanelViewController in an iOS app for drawing left and right controllers (As android has inbuilt control drawer). It works good but it does not come with gesture it comes little late. if anyone had an answer to problem it would be great.
This is the code in UIViewController (MCPanelViewControllerInternal) category:
- (void)handlePan:(UIPanGestureRecognizer *)pan {
// initialization for screen edge pan gesture
if ([pan isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] &&
pan.state == UIGestureRecognizerStateBegan) {
__weak UIViewController *controller = objc_getAssociatedObject(pan, &MCPanelViewGesturePresentingViewControllerKey);
if (!controller) {
return;
}
MCPanelAnimationDirection direction = [objc_getAssociatedObject(pan, &MCPanelViewGestureAnimationDirectionKey) integerValue];
[self setupController:controller withDirection:direction];
CGPoint translation = [pan translationInView:pan.view];
CGFloat width = direction == MCPanelAnimationDirectionLeft ? translation.x : -1 * translation.x;
[self layoutSubviewsToWidth:0];
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:MCPanelViewAnimationDuration delay:0 options:0 animations:^{
typeof(self) strongSelf = weakSelf;
[strongSelf layoutSubviewsToWidth:width];
} completion:^(BOOL finished) {
}];
CGFloat offset = self.maxWidth - width;
if (direction == MCPanelAnimationDirectionLeft) {
offset *= -1;
}
[pan setTranslation:CGPointMake(offset, translation.y) inView:pan.view];
}
if (!self.parentViewController) {
return;
}
CGFloat newWidth = [pan translationInView:pan.view].x;
if (self.direction == MCPanelAnimationDirectionRight) {
newWidth *= -1;
}
newWidth += self.maxWidth;
CGFloat ratio = newWidth / self.maxWidth;
switch (pan.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged: {
[self layoutSubviewsToWidth:newWidth];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
CGFloat threshold = MCPanelViewGestureThreshold;
// invert threshold if we started a screen edge pan gesture
if ([pan isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]) {
threshold = 1 - threshold;
}
if (ratio < threshold) {
[self dismiss];
}
else {
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:MCPanelViewAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
typeof(self) strongSelf = weakSelf;
[strongSelf layoutSubviewsToWidth:strongSelf.maxWidth];
} completion:^(BOOL finished) {
}];
}
break;
}
default:
break;
}
}
This is a known issue of the library.
To quote matthewcheok:
The delay is mainly due to way the panel captures the background image to apply the image blur in advance. It does this exactly once, when the gesture is triggered or when opened programatically, and then crops the image using UIView content modes, to show only the relevant parts obscured by the panel.
I'm developing an iOS6 app for iPad. I have coded a subclass of UITextField that enables the user to drag, pinch and rotate the field around the view. The problem is that if you pinch the field with no rotation, after finishing the gesture, it enters in editing mode. This should not happen, and I've added some lines that disable that, but it doesn't stop. Here I my code:
- (BOOL) canBecomeFirstResponder{
if (gesturing==YES) {
return NO;
}else return YES;
}
- (void) tapDetected:(UITapGestureRecognizer*) pinchRecognizer {
if (gesturing==NO) {
[self becomeFirstResponder];
}
}
- (void) pinchDetected:(UIPinchGestureRecognizer*) pinchRecognizer {
CGFloat scale = pinchRecognizer.scale;
self.font = [self.font fontWithSize:self.font.pointSize*(scale)];
//self.transform = CGAffineTransformScale(self.transform, scale, scale);
[self sizeToFit];
pinchRecognizer.scale = 1.0;
if (pinchRecognizer.state == UIGestureRecognizerStateChanged) {
gesturing =YES;
}
if (pinchRecognizer.state == UIGestureRecognizerStateBegan) {
gesturing =YES;
}
if (pinchRecognizer.state == UIGestureRecognizerStateEnded) {
gesturing =NO;
}
}
- (void) panDetected:(UIPanGestureRecognizer *)panRecognizer {
CGPoint translation = [panRecognizer translationInView:self.superview];
CGPoint imageViewPosition = self.center;
imageViewPosition.x += translation.x;
imageViewPosition.y += translation.y;
self.center = imageViewPosition;
[panRecognizer setTranslation:CGPointZero inView: self.superview];
if (panRecognizer.state == UIGestureRecognizerStateChanged) {
gesturing=YES;
}
if (panRecognizer.state == UIGestureRecognizerStateEnded) {
gesturing=NO;
}
}
- (void) rotationDetected:(UIRotationGestureRecognizer *)rotationRecognizer{
CGFloat angle = rotationRecognizer.rotation;
self.transform = CGAffineTransformRotate(self.transform, angle);
rotationRecognizer.rotation = 0.0;
if (rotationRecognizer.state == UIGestureRecognizerStateChanged) {
gesturing = YES;
}
if (rotationRecognizer.state == UIGestureRecognizerStateEnded) {
gesturing = NO;
}
}
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I bydefault value of gesturing is NO, if you don't initialize it to YES. Have debugged and figured out which gesture recognizer is detected first whenever you try to apply pinch?
You support simultaneous gesture recognition and hence when you put to apply pinch, your finger's touch may get detected as pan. If it is the case then if(gesturing == NO) condition in tapDetected: method gets satisfied and your textField becomes first responder.