Scroll effect with swipe gesture in iOS - iphone

I have a view which has two labels. When I swipe left I fill next content to label text. Similarly swiping right loads previous content. I want to give an effect to labels like they are scrolling from left or right.
I used a scrollview before but it had a memory problem. So I'm using one view, and swipe gesture loads next or previous content. I want to add scrollview's sliding effect to labels. How can I do that?

I'm not quite sure precisely what effect you're looking for, but you could do something like this, which creates a new, temporary label, puts it off screen, animates the moving it over the label you have on screen, and then when done, resets the old one and deletes the temporary label. This is what a non-autolayout implementation might look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UISwipeGestureRecognizer *left = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(leftSwipe:)];
[left setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.view addGestureRecognizer:left];
// if non-ARC, release it
// [release left];
self.label1.text = #"Mo";
}
- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
NSString *newText;
UILabel *existingLabel = self.label1;
// in my example, I'm just going to toggle the value between Mo and Curly
if ([existingLabel.text isEqualToString:#"Curly"])
newText = #"Mo";
else
newText = #"Curly";
// create new label
UILabel *tempLabel = [[UILabel alloc] initWithFrame:existingLabel.frame];
[existingLabel.superview addSubview:tempLabel];
tempLabel.text = newText;
// move the new label off-frame to the right
tempLabel.transform = CGAffineTransformMakeTranslation(tempLabel.superview.bounds.size.width, 0);
// animate the sliding of them into place
[UIView animateWithDuration:0.5
animations:^{
tempLabel.transform = CGAffineTransformIdentity;
existingLabel.transform = CGAffineTransformMakeTranslation(-existingLabel.superview.bounds.size.width, 0);
}
completion:^(BOOL finished) {
existingLabel.text = newText;
existingLabel.transform = CGAffineTransformIdentity;
[tempLabel removeFromSuperview];
}];
// if non-ARC, release it
// [release tempLabel];
}
This animation animates the label with respect to its superview. You may want to ensure that the superview is set to "clip subviews". This way, the animation will be constrained to the bounds of that superview, which yields a slightly more polished look.
Note, if using auto layout, the idea is the same (though the execution is more complicated). Basically configure your constraints so new view is off to the right, then, in animation block update/replace the constraints so the original label is off to the left and the new one is in the spot of the original label, and, finally, in the completion block reset the constraints of the original label and remove the temporary label.
By the way, this is all infinitely easier if you're comfortable with one of the built in transitions:
- (void)leftSwipe:(UISwipeGestureRecognizer *)gesture
{
NSString *newText;
UILabel *existingLabel = self.label1;
// in my example, I'm just going to toggle the value between Mo and Curly
if ([existingLabel.text isEqualToString:#"Curly"])
newText = #"Mo";
else
newText = #"Curly";
[UIView transitionWithView:existingLabel // or try `existingLabel.superview`
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^{
existingLabel.text = newText;
}
completion:nil];
}

If you prefer animation that behaves more like a scroll view (i.e., with continuous feedback on the gesture), it might look something like the following:
- (void)viewDidLoad
{
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panHandler:)];
[self.view addGestureRecognizer:pan];
self.label1.text = #"Mo";
}
- (void)panHandler:(UIPanGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateBegan)
{
_panLabel = [[UILabel alloc] init];
// in my example, I'm just going to toggle the value between Mo and Curly
// you'll presumably set the label contents based upon the direction of the
// pan (if positive, swiping to the right, grab the "previous" label, if negative
// pan, grab the "next" label)
if ([self.label1.text isEqualToString:#"Curly"])
_newText = #"Mo";
else
_newText = #"Curly";
// set the text
_panLabel.text = _newText;
// set the frame to just be off screen
_panLabel.frame = CGRectMake(self.label1.frame.origin.x + self.containerView.frame.size.width,
self.label1.frame.origin.y,
self.label1.frame.size.width,
self.label1.frame.size.height);
[self.containerView addSubview:_panLabel];
_originalCenter = self.label1.center; // save where the original label originally was
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
CGPoint translate = [sender translationInView:self.containerView];
if (translate.x > 0)
{
_panLabel.center = CGPointMake(_originalCenter.x - self.containerView.frame.size.width + translate.x, _originalCenter.y);
self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
}
else
{
_panLabel.center = CGPointMake(_originalCenter.x + self.containerView.frame.size.width + translate.x, _originalCenter.y);
self.label1.center = CGPointMake(_originalCenter.x + translate.x, _originalCenter.y);
}
}
else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateFailed || sender.state == UIGestureRecognizerStateCancelled)
{
CGPoint translate = [sender translationInView:self.containerView];
CGPoint finalNewFieldLocation;
CGPoint finalOriginalFieldLocation;
BOOL panSucceeded;
if (sender.state == UIGestureRecognizerStateFailed ||
sender.state == UIGestureRecognizerStateCancelled)
{
panSucceeded = NO;
}
else
{
// by factoring in the velocity, we can capture a flick more accurately
//
// (by the way, I don't like iOS's velocity, because if you stop moving, it records the velocity
// prior to stopping the move rather than noting that you actually stopped, so I usually calculate my own,
// but I'll leave this as is for purposes of this example)
CGPoint velocity = [sender velocityInView:self.containerView];
if (translate.x < 0)
panSucceeded = ((translate.x + velocity.x * 0.5) < -(self.containerView.frame.size.width / 2));
else
panSucceeded = ((translate.x + velocity.x * 0.5) > (self.containerView.frame.size.width / 2));
}
if (panSucceeded)
{
// if we succeeded, finish moving the stuff
finalNewFieldLocation = _originalCenter;
if (translate.x < 0)
finalOriginalFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
else
finalOriginalFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
}
else
{
// if we didn't, then just return everything to where it was
finalOriginalFieldLocation = _originalCenter;
if (translate.x < 0)
finalNewFieldLocation = CGPointMake(_originalCenter.x + self.containerView.frame.size.width, _originalCenter.y);
else
finalNewFieldLocation = CGPointMake(_originalCenter.x - self.containerView.frame.size.width, _originalCenter.y);
}
// animate the moving of stuff to their final locations, and on completion, clean everything up
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
_panLabel.center = finalNewFieldLocation;
self.label1.center = finalOriginalFieldLocation;
}
completion:^(BOOL finished) {
if (panSucceeded)
self.label1.text = _newText;
self.label1.center = _originalCenter;
[_panLabel removeFromSuperview];
_panLabel = nil; // in non-ARC, release instead
}
];
}
}
Note, I have put both the original label, as well as the new label being panned on, in a container UIView (called containerView, surprisingly enough), so that I can clip the animation to that container.

Related

UIPercentDrivenInteractiveTransition cancelTransition turns screen black

I'm implementing custom navigation transitions, and using UIPercentDrivenInteractiveTransition to do the trick.
I've got the noninteractive transition working fine, and I'm using an animation block to handle the transition animation, but once I added in interactive transitions, the screen now goes black if I cancel the transition.
Here's my code that tracks the gesture:
-(void)userDidPan:(UIScreenEdgePanGestureRecognizer *)recognizer {
CGPoint location = [recognizer locationInView:nil];
if (recognizer.state == UIGestureRecognizerStateBegan) {
// We're being invoked via a gesture recognizer – we are necessarily interactive
self.hasActiveInteraction = YES;
self.interactiveTransition = [BXTPercentDrivenInteractiveTransition new];
[[BXTNavigationController sharedNavigationController] popViewControllerAnimated:YES];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGFloat ratio = location.x / CGRectGetWidth(recognizer.view.frame);
NSLog(#"Percentage complete: %0.2f",ratio*100);
[self.interactiveTransition updateInteractiveTransition:ratio];
}
else if (recognizer.state == UIGestureRecognizerStateEnded) {
// Depending on our state and the velocity, determine whether to cancel or complete the transition.
CGFloat ratio = location.x / CGRectGetWidth(recognizer.view.frame);
if (ratio > 0.50) {
NSLog(#"Completing");
[self.interactiveTransition finishInteractiveTransition];
}
else {
NSLog(#"Canceling");
[self.interactiveTransition cancelInteractiveTransition];
}
}
}
...and here's an (abbreviated) look at my animation block when popping off the view controller:
[UIView animateWithDuration:kBXTNavigationTimingDuration
delay:0
options:animationOption
animations:^{
toVC.view.frame = destinationFrameForPoppedView;
fromVC.view.frame = CGRectOffset(fromVC.view.frame, fromVC.view.frame.size.width, 0);
} completion:^(BOOL finished) {
[darkeningView removeFromSuperview];
[_transition.secondaryTransitionViews enumerateObjectsUsingBlock:^(BXTTransitionView *transitionView, NSUInteger idx, BOOL *stop) {
[transitionView.view removeFromSuperview];
}];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
Any ideas why the screen goes black when cancelling a swipe-from-the-left pop animation with a custom navigation transition?
Solved! I deleted all the removeFromSuperview calls in the animation completion block and it solved it (I had the same problem like you with almost the same code, see my comment to your q'). Try removing these lines from your code:
[darkeningView removeFromSuperview];
[_transition.secondaryTransitionViews enumerateObjectsUsingBlock:^(BXTTransitionView *transitionView, NSUInteger idx, BOOL *stop) {
[transitionView.view removeFromSuperview];
}];

How to make an UICollectionView with infinite paging?

I have a UICollectionView with 6 pages, and paging enabled, and a UIPageControl. What I want is, when I came to the last page, if I drag to right, UICollectionView reloads from first page seamlessly.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
// The key is repositioning without animation
if (collectionView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image 10.
// reposition offset to show image 10 that is on the right in the scroll view
[collectionView scrollRectToVisible:CGRectMake(collectionView.frame.size.width*(pageControl.currentPage-1),0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
else if (collectionView.contentOffset.x == 1600) {
// user is scrolling to the right from image 10 to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[collectionView scrollRectToVisible:CGRectMake(0,0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
pageControlUsed = NO;
}
It doesn't work like I want. What can I do?
Here's what I ended up with for my UICollectionView (horizontal scrolling like the UIPickerView):
#implementation UIInfiniteCollectionView
- (void) recenterIfNecessary {
CGPoint currentOffset = [self contentOffset];
CGFloat contentWidth = [self contentSize].width;
// don't just snap to center, since this might be done in the middle of a drag and not aligned. Make sure we account for that offset
CGFloat offset = kCenterOffset - currentOffset.x;
int delta = -round(offset / kCellSize);
CGFloat shift = (offset + delta * kCellSize);
offset += shift;
CGFloat distanceFromCenter = fabs(offset);
// don't always recenter, just if we get too far from the center. Eliza recommends a quarter of the content width
if (distanceFromCenter > (contentWidth / 4.0)) {
self.contentOffset = CGPointMake(kCenterOffset, currentOffset.y);
// move subviews back to make it appear to stay still
for (UIView *subview in self.subviews) {
CGPoint center = subview.center;
center.x += offset;
subview.center = center;
}
// add the offset to the index (unless offset is 0, in which case we'll assume this is the first launch and not a mid-scroll)
if (currentOffset.x > 0) {
int delta = -round(offset / kCellSize);
// MODEL UPDATE GOES HERE
}
}
}
- (void) layoutSubviews { // called at every frame of scrolling
[super layoutSubviews];
[self recenterIfNecessary];
}
#end
Hope this helps someone.
I've been using the Street Scroller sample to create an infinite scroller for images. That works fine until I wanted to set pagingEnabled = YES; Tried tweaking around the recenterIfNecessary code and finally realized that it's the contentOffset.x that has to match the frame of the subview that i want visible when paging stops. This really isn't going to work in recenterIfNecessary since you have no way of knowing it will get called from layoutSubviews. If you do get it adjusted right, the subview may pop out from under your finger. I do the adjustment in scrollViewDidEndDecelerating. So far I haven't had problems with scrolling fast. It will work and simulate paging even when pagingEnabled is NO, but it looks more natural with YES.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[super scrollViewDidEndDecelerating:scrollView];
CGPoint currentOffset = [self contentOffset];
// find the subview that is the closest to the currentOffset.
CGFloat closestOriginX = 999999;
UIView *closestView = nil;
for (UIView *v in self.visibleImageViews) {
CGPoint origin = [self.imageContainerView convertPoint:v.frame.origin toView:self];
CGFloat distanceToCurrentOffset = fabs(currentOffset.x - origin.x);
if (distanceToCurrentOffset <= closestOriginX) {
closestView = v;
closestOriginX = distanceToCurrentOffset;
}
}
// found the closest view, now find the correct offset
CGPoint origin = [self.imageContainerView convertPoint:closestView.frame.origin toView:self];
CGPoint center = [self.imageContainerView convertPoint:closestView.center toView:self];
CGFloat offsetX = currentOffset.x - origin.x;
// adjust the centers of the subviews
[UIView animateWithDuration:0.1 animations:^{
for (UIView *v in self.visibleImageViews) {
v.center = [self convertPoint:CGPointMake(v.center.x+offsetX, center.y) toView:self.imageContainerView];
}
}];
}
I have not used UICollectionView for infinite scrolling, but when doing it with a UIScrollView you first adjust your content offset (instead of using scrollRectToVisible) to the location you want. Then, you loop through each subview in your scroller and adjust their coordinates either to the right or left based on the direction the user was scrolling. Finally, if either end is beyond the bounds you want them to be, move them to the far other end. Their is a very good WWDC video from apple about how to do infinite scrolling you can find here: http://developer.apple.com/videos/wwdc/2012/

Swapping images in IOS

I have to swap four images on a single screen . The images can be swapped only in left/right/upwards/downwards directions only and not diagonally . For example , the 1st image can be swapped with the images on its right and below it , the 2nd image can be swapped only with the one on the left and below it , and so on . Can anyone please help me about how to do it . Thanks
To add drag and drop functionality, assuming that is what you mean, you'll want to look into UIPanGestureRecognizer
Add a swipe gesture recognizer. When the user swipes determine which direction and handle the image swap.
[EDIT] - Imagine a square divided into four equal sections. The top left section has an index of zero, the top right an index of one, the bottom left an index of 2 and finally the bottom right an index of 3. The code below checks the current index and from that it determines if an image swap can be made, if not it does nothing.
This code is from the top of my head so there may be syntax errors but the logic is sound (I hope :D ).
- (void) viewDidLoad {
// turn on user interaction on the image view as its off by default.
[self.imageView setUserInteractionEnabled:TRUE];
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipe:)];
[recognizer setDirection:(UISwipeGestureRecognizerDirectionRight | UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionUp)];
[self.imageView addGestureRecognizer:recognizer];
self.currentImageIndex = 0;
self.images = [NSArray arrayWithObjects:[UIImage imageNamed:#"top-left"],[UIImage imageNamed:#"top-right"],[UIImage imageNamed:#"bottom-left"],[UIImage imageNamed:#"top-right"],nil];
}
-(void)handleSwipeFrom:(UISwipeGestureRecognizer *)recognizer {
if (recognizer.direction == UISwipeGestureRecognizerDirectionRight) {
if (self.currentImageIndex == 0 || self.currentImageIndex == 2) self.currentImageIndex++; // change to image to the right
else return; // do nothing
}
else if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
if (self.currentImageIndex == 1 || self.currentImageIndex == 3) self.currentImageIndex--; // change to the image to the left
else return; // do nothing
}
else if (recognizer.direction == UISwipeGestureRecognizerDirectionUp) {
if (self.currentImageIndex == 2 || self.currentImageIndex == 3) self.currentImageIndex -= 2; // change to the above image
else return; // do nothing
}
else if (recognizer.direction == UISwipeGestureRecognizerDirectionDown) {
if (self.currentImageIndex == 0 || self.currentImageIndex == 1) self.currentImageIndex += 2; // change to the above image
else return; // do nothing
}
[UIView animationWithDuration:0.5 animations:^{
[self.imageView setAlpha:0];
} completion^(BOOL finished){
if (finished) {
[UIView animationWithDuration:0.5 animations:^{
[self.imageView setImage[self.images objectAtIndex:self.currentImageIndex]];
[self.imageView setAlpha:1];
}];
}
}];
}

Insert multiple UIImages into a "container"

I'd like to know if it's possible to insert multiple images into a "container" in objective-c.
For example, if i had an image of a front wheel, a back wheel, and the body of a car. Could i position these all in a container so that when i move or tranform (scale, rotate) the container, all the images inside will perform the that action? At the same time i could have one or more of the images inside the container performing an animation.
I hope that's clear and i've properly explained what i'm trying to do.
Not sure about container,
But I have another approach,
Why don't you make one custom view and put all the images inside that, and can perform all the operation over that view,
I am posting some important method, let me know if its works for you, i may share some more detail
-(void)refreshSession{
// check all the added view, if needs to be displayed, then displayed them.
int visibleViews = [self getVisibleViews];
if(visibleViews == 0 ){
// if no more views to be displayed, then then let it not be visible,
[[self window] orderOut:self];
// do it will not be visible to anyone
return;
}
[self resize:visibleViews];
int viewsPerSide = [self getViewsPerRow];//ceil(sqrt(count));
int count = [pViewArray count];
int viewIndex=0;
NSInteger index = 0;
NSPoint curPoint = NSZeroPoint;
// Starting at the bottom left corner
// we should check all visible views
for ( int i = 0; i < count ; i++ ) {
// it will give only the view that needs to be shown
NSView *subview = [self getViewAtIndex:i];//(NSView *)[pViewArray objectAtIndex:i];
// is this visible
if(![self isViewVisibleAtIndex:i])
continue;
// is view visible
// if we have view to display then lets display it.
if(subview){
//[[[self window] contentView]setBackgroundColor:[NSColor blackColor]];
[[[self window]contentView] addSubview:subview ];
[self checkMask:viewIndex view:subview];
viewIndex++;
// let all the frame have same rect, regardless what are those
NSRect frame = NSMakeRect(curPoint.x, curPoint.y, BOXWIDTH, BOXHEIGHT);
// add them into the subview
NSRect newFrame = [self integralRect:frame];
[subview setFrame:newFrame];
[[subview animator] setFrame:[self integralRect:frame]];
[self setSubViewMask:subview ContentView:[[self window]contentView]];
// Now set the point
curPoint.x += BOXWIDTH + SEPARATION; // Stack them horizontally
if ((++index) % viewsPerSide == 0) { // And if we have enough on this row, move up to the next
curPoint.x = 0;
curPoint.y += BOXHEIGHT + SEPARATION;
}
}
}
NSArray *pSubViewsArray = [[[self window]contentView]subviews];
int arrayCount = [pSubViewsArray count];
debugLog<<"Array Count"<<arrayCount<<endl;
}

Check direction of scroll in UIScrollView

I am trying to implement the method scrollViewWillBeginDragging.
When this method is called I check that the user has selected one of the buttons that are within the scrollview is in state:selected.
If not then I display a UIAlert to inform the user.
My problem here is I only want to call the button selected (NextQuestion) method if the user scrolls from right to left (pulling the next view from the right).
But if they are scrolling from Left to Right then I want it to scroll as normal.
At present the checker method is called no matter what direction the user scrolls in. How can I only call a method when they scroll from Right To Left?
Here is how i've currently implemented it:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self NextQuestion:scrollView];
}
-(IBAction)NextQuestion:(id)sender
{
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth) / pageWidth) + 1;
NSInteger npage = 0;
CGRect frame = scrollView.frame;
// Check to see if the question has been answered. Call method from another class.
if([QuestionControl CheckQuestionWasAnswered:page])
{
pageNumber++;
NSLog(#"Proceed");
if(([loadedQuestionnaire count] - 1) != page)
{
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
}
// update the scroll view to the appropriate page
frame = scrollView.frame;
frame.origin.x = (frame.size.width * page) + frame.size.width;
frame.origin.y = 0;
[self.scrollView scrollRectToVisible:frame animated:YES];
}
// If question has not been answered show a UIAlert with instructions.
else
{
UIAlertView *alertNotAnswered = [[UIAlertView alloc] initWithTitle:#"Question Not Answered"
message:#"You must answer this question to continue the questionnaire."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil, nil];
[alertNotAnswered show];
}
}
Code for Solution 1:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"%f",scrollView.contentOffset.x);
if (scrollView.contentOffset.x < lastOffset) // has scrolled left..
{
lastOffset = scrollView.contentOffset.x;
[self NextQuestion:scrollView];
}
}
In scrollViewWillBeginDragging the scroll view has not yet moved (or registered the move) and so contentOffset will by 0. As of IOS 5 you can instead look in the scrollview's panGestureRecognizer to determine the direction and magnitude of the user's scrolling gesture.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer translationInView:scrollView.superview];
if(translation.x > 0)
{
// react to dragging right
} else
{
// react to dragging left
}
}
Make a CGFloat lastOffset as a member variable in your class.h file..
then set it 0 in viewDidLoad .
then check in
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x < lastOffset) // has scrolled left..
{
lastOffset = scrollView.contentOffset.x;
[self NextQuestion:scrollView];
}
}
You can keep a starting offset as a member of your class which you store when the delegate receives the scrollViewDidBeginDragging: message.
Once you have that value you can compare the x value of the scroll view offset with the one you have stored and see whether the view was dragged left or right.
If changing direction mid-drag is important, you can reset your compared point in the viewDidScroll: delegate method. So a more complete solution would store both the last detected direction and the base offset point and update the state every time a reasonable distance has been dragged.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat distance = lastScrollPoint.x - scrollView.contentOffset.x;
NSInteger direction = distance > 0 ? 1 : -1;
if (abs(distance) > kReasonableDistance && direction != lastDirection) {
lastDirection = direction;
lastScrollPoint = scrollView.contentOffset;
}
}
The 'reasonable distance' is whatever you need to prevent the scrolling direction to flip between left and right to easily but about 10 points should be about enough.