Resetting the Zoom on a UIPageViewController - ios

I am building up a simple application that is made up of a UITableViewController with languages and when a specific cell is clicked, a UIPageViewController is brought up to represent the images for that selected language. The user can scroll through the images and everything works as desired. The next step was to build a zooming capability into the UIPageViewController so the user could zoom into the images with a pinch gesture.
I have achieved this with the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.leafletImages = [NSMutableArray arrayWithObjects:[[ImageModel alloc] initWithImageName:#"3facts-chinese-page1.jpg"], [[ImageModel alloc] initWithImageName:#"3facts-chinese-page2.jpg"], [[ImageModel alloc] initWithImageName:#"3facts-chinese-page3.jpg"], [[ImageModel alloc] initWithImageName:#"3facts-chinese-page4.jpg"], [[ImageModel alloc] initWithImageName:#"3facts-chinese-page5.jpg"], [[ImageModel alloc] initWithImageName:#"3facts-chinese-page6.jpg"], nil];
// Lots of code for the building up of the UIPageViewController
LeafletImageSizeViewController *imageViewController = [[LeafletImageSizeViewController alloc] init];
imageViewController.model = [_modelArray objectAtIndex:0];
NSArray *viewControllers = [NSArray arrayWithObject:imageViewController];
[self.pageViewController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
// Gesture
UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchDetected:)];
[self.view addGestureRecognizer:pinchRecognizer];
pinchRecognizer.delegate=self;
}
The class creating the image and the size is:
- (void)useThreeFactsSize
CGRect insetFrame;
insetFrame = CGRectMake(310, 70, self.view.frame.size.width-615, self.view.frame.size.height-85);
_imageView = [[UIImageView alloc] initWithFrame:insetFrame];
[_imageView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
[_imageView setImage:[UIImage imageNamed:_model.imageName]];
[[self view] addSubview:_imageView];
[self.view setBackgroundColor:[UIColor clearColor]];
}
The pinchDetection method is:
-(void)pinchDetected:(UIPinchGestureRecognizer *)pinchRecognizer
{
CGFloat scale = pinchRecognizer.scale;
self.pageViewController.view.transform = CGAffineTransformScale(self.pageViewController.view.transform, scale, scale);
pinchRecognizer.scale = 1.0;
}
Now, I can zoom into the images of the UIPageViewController without any issues and it works really well.
What I want to do however is two things:
Not allow the image to be zoomed out beyond the original scale
Create a double tap gesture to bring the image back to it's original scale
With feature 1, the user can zoom into the image, but also completely zoom out of the image which shrinks the image and the UIPageViewController pageIndicators. There's no reason the user should be able to zoom out of the image, so I'd like to allow the user to zoom in to any scale, but not to zoom out beyond what the original size of the image on screen in the UIPageViewController.
With feature 2, I'd like to implement a gesture to double tap the screen and for the zoomed image to go back to it's original scale (like the Photos.app).
Update
With reference to the answer, I have updated the question to reflect how I'm going about doing the images. With point 2 and the double tap gesture, the following code almost works:
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer *)recognizer
{
NSLog(#"Double Tap");
self.pageViewController.view.transform = CGAffineTransformIdentity;
}
What it's currently doing is if I zoom in with a pinch and pan around, and then double tap, it centres the image to the point of where I tapped, so sometimes the borders are being shown, etc, rather than making the image centre to where it's supposed to be.
For point 1:
if (pinchRecognizer.scale > 1) {
CGFloat scale = pinchRecognizer.scale;
self.pageViewController.view.transform = CGAffineTransformScale(self.pageViewController.view.transform, scale, scale);
pinchRecognizer.scale = 1.0;
}
If I have self.imageview, it doesn't work because it's nil and even if I make a call to the class setting the size, it's nil as well.
I suspect I have a number of things wrong with my code!
For reference, I have panning working with:
- (void)panGestureDetected:(UIPanGestureRecognizer *)recognizer
{
UIGestureRecognizerState state = [recognizer state];
if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [recognizer translationInView:recognizer.view];
[recognizer.view setTransform:CGAffineTransformTranslate(recognizer.view.transform, translation.x, translation.y)];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
else if(state==UIGestureRecognizerStateEnded){
UIView *imageView = recognizer.view;
UIView *container = imageView.superview;
CGFloat targetX = CGRectGetMinX(imageView.frame);
CGFloat targetY = CGRectGetMinY(imageView.frame);
if(targetX>0){
// targetX = 0;
}else if(CGRectGetMaxX(imageView.frame)<CGRectGetWidth(container.bounds)){
targetX = CGRectGetWidth(container.bounds)-CGRectGetWidth(imageView.frame);
}
if(targetY>0){
// targetY = 0;
}else if(CGRectGetMaxY(imageView.frame)<CGRectGetHeight(container.bounds)){
// targetY = CGRectGetHeight(container.bounds)-CGRectGetHeight(imageView.frame);
}
// imageView.frame = CGRectMake(targetX, targetY, CGRectGetWidth(imageView.frame), CGRectGetHeight(imageView.frame));
[UIView animateWithDuration:0.3 animations:^{
imageView.frame = CGRectMake(targetX, targetY, CGRectGetWidth(imageView.frame), CGRectGetHeight(imageView.frame));
}];
}
}
That's working very well at the moment, but there's definitely a conflict with everything else.
I'd really appreciate any guidance in the right direction on this.

1.) To not allow the image to be zoomed out beyond it's original scale you first just need to check if the scale you're about to set it to is greater than 1 or not. If it's less than one, you don't want to rescale your image as that would mean it gets smaller. So...
#IBAction func doPinch(sender: UIPinchGestureRecognizer) {
if sender.scale > 1 {
let transform = CGAffineTransformMakeScale(sender.scale, sender.scale)
imageView.transform = transform
}
}
2.) You seem to be changing the view's frame to try and change it's scale, but you never adjusted the view's frame yourself in the first place. You're adjusting the view's transform. That means in order to return it to it's original size you must remove whatever transform you put on it. To do this you put it back to it's identity. So...
#IBAction func doDoubleTap(sender: UITapGestureRecognizer) {
imageView.transform = CGAffineTransformIdentity
}
My code samples are in Swift but you should be able to adjust it to Objective-C yourself. Also, you seem to be adjusting the transform/scale of the entire page view itself. I would suggest you change the scale of only the image view. That makes more sense as that's actually what you're trying to zoom into.

Related

How do I calculate the correct CGRect origin on a scaled UIView subview?

I need to calculate the visible CGRect of a UIView subview, in the coordinates of the original view. I've got it working if the scale is 1, but if one of the superviews or the view itself is scaled (pinch), the visible CGRect origin is offset slightly.
This works when the scale of the views is 1 or the view is a subview of the root view:
// return the part of the passed view that is visible
// TODO: figure out why result origin is wrong for scaled subviews
//
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view frame
}
When a subview is scaled, the size of the resultant view is correct, but the origin is offset by a small amount. It appears that the convertRect does not calculate the origin properly for scaled subviews.
I tried adjusting the origin relative to the X/Y transform scale but I could not get the calculation correct. Perhaps someone can help?
To save time, here is a complete test ViewController.m, where a box with an X is drawn on the visible part of the views - just create a reset button in the Main.storyboard and connect it to the reset method:
//
// ViewController.m
// VisibleViewDemo
//
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
CG_INLINE void drawLine(UIView *view,CGPoint point1,CGPoint point2, UIColor *color, NSString *layerName) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:point1];
[path addLineToPoint:point2];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineWidth = 2.0;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.name = layerName;
[view.layer addSublayer:shapeLayer];
}
CG_INLINE void removeShapeLayers(UIView *view,NSString *layerName) {
if (view.layer.sublayers.count > 0) {
for (CALayer *layer in [view.layer.sublayers copy]) {
if ([layer.name isEqualToString:layerName]) {
[layer removeFromSuperlayer];
}
}
}
}
CG_INLINE void drawXBox(UIView *view, CGRect rect,UIColor *color) {
NSString *layerName = #"xbox";
removeShapeLayers(view, layerName);
CGPoint topLeft = CGPointMake(rect.origin.x,rect.origin.y);
CGPoint topRight = CGPointMake(rect.origin.x + rect.size.width,rect.origin.y);
CGPoint bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
CGPoint bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
drawLine(view,topLeft,topRight,color,layerName);
drawLine(view,topRight,bottomRight,color,layerName);
drawLine(view,topLeft,bottomLeft,color,layerName);
drawLine(view,bottomLeft,bottomRight,color,layerName);
drawLine(view,topLeft,bottomRight,color,layerName);
drawLine(view,topRight,bottomLeft,color,layerName);
}
#interface ViewController ()
#end
#implementation ViewController
UIView *view1;
UIView *view2;
UIView *view3;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2;
CGFloat height = [UIScreen mainScreen].bounds.size.height / 4;
view1 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2, width, height)];
view1.backgroundColor = UIColor.yellowColor;
[self.view addSubview:view1];
[self addGestures:view1];
view2 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2 + height + 16, width, height)];
view2.backgroundColor = UIColor.greenColor;
[self.view addSubview:view2];
[self addGestures:view2];
view3 = [[UIView alloc] initWithFrame:CGRectMake(10, 10, width / 2, height / 2)];
view3.backgroundColor = [UIColor.blueColor colorWithAlphaComponent:0.5];
[view1 addSubview:view3]; // this one will behave differently
[self addGestures:view3];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self checkOnScreen:view1];
[self checkOnScreen:view2];
[self checkOnScreen:view3];
}
- (IBAction)reset:(id)sender {
view1.transform = CGAffineTransformIdentity;
view2.transform = CGAffineTransformIdentity;
view3.transform = CGAffineTransformIdentity;
[self.view setNeedsLayout];
}
- (void)addGestures:(UIView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[view addGestureRecognizer:panGestureRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePinch:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view
}
- (void)checkOnScreen:(UIView *)view {
CGRect visibleRect = [self getVisibleRect:view];
if (CGRectEqualToRect(visibleRect, CGRectNull)) {
visibleRect = CGRectZero;
}
drawXBox(view,visibleRect,UIColor.blackColor);
}
//
// Pinch (resize) an image on the ViewController View
//
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.transform = CGAffineTransformTranslate(initialTransform,translation.x,translation.y);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
#end
So you need to know the real visible frame of a view that is somehow derived from bounds+center+transform and calculate everything else from that, instead of the ordinary frame value. This means you'll also have to recreate convertRect:fromView: to be based on that. I always sidestepped the problem by using transform only for short animations where such calculations are not necessary. Thinking about coding such a -getVisibleRect: method makes me want to run away screaming ;)
What is a frame?
The frame property is derived from center and bounds.
Example:
center is (60,50)
bounds is (0,0,100,100)
=> frame is (10,0,100,100)
Now you change the frame to (10,20,100,100). Because the size of the view did not change, this results only in a change to the center. The new center is now (60,70).
How about transform?
Say you now transform the view, by scaling it to 50%.
=> the view has now half the size than before, while still keeping the same center. It looks like the new frame is (35,45,50,50). However the real result is:
center is still (60,50): this is expected
bounds is still (0,0,100,100): this should be expected too
frame is still (10,20,100,100): this is somewhat counterintuitive
frame is a calculated property, and it doesn't care at all about the current transform. This means that the value of the frame is meaningless whenever transform is not the identity transform. This is even documented behaviour. Apple calls the value of frame to be "undefined" in this case.
Consequences
This has the additional consequences that methods such as convertRect:fromView: do not work properly when there are non-standard transforms involved. This is because all these methods rely on either frame or bounds of views, and they break as soon as there are transforms involved.
What can be done?
Say you have three views:
view1 (no transform)
view2 (scale transform 50%)
view3 (no transform)
and you want to know the coordinates of view3 from the point of view of view1.
From the point of view of view2, view3 has frame view3.frame. Easy.
From the point of view of view1, view2 has not frame view2.frame, but the visible frame is a rectangle with size view2.bounds/2 and center view2.center.
To get this right you need some basic linear algebra (with matrix multiplications). (And don't forget the anchorPoint..)
I hope it helps..
What can be done for real?
In your question you said that there is an offset. Maybe you can just calculate the error now? The error should be something like 0.5 * (1-scale) * (bounds.size) . If you can calculate the error, you can subtract it and call it a day :)
Thanks to #Michael for putting in so much effort in his answer. It didn't solve the problem but it made me think some more and try some other things.
And voila, I tried something that I'm certain I had done before, but this time I started with my latest code. It turns out a simple solution did the trick. The builtin UIView convertRect:fromView and convertRect:toView worked as expected when used together.
I apologize to anyone that has spent time on this. I'm humbled in my foolishness and how much time I have spent on this. I must have made a mistake somewhere when I tried this before because it didn't work. But this works very well now:
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect rootRect = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect rootVisible = CGRectIntersection(vc.view.bounds, rootRect);
// convert the rect back to the initial view's coordinate system
CGRect visible = [view convertRect:rootVisible fromView:vc.view];
return visible; // may be same as the original view frame
}
If someone uses the Viewcontroller.m from my question, just replace the getVisibleRect method with this one and it will work very nicely.
NOTE: I tried rotating the view and the visible rect is rotated too because I displayed it on the view itself. I guess I could reverse whatever the view rotation is on the shape layers, but that's for another day!

Change View Alpha on Pan Gesture or Dragging

I want to a UIView to drag to bottom of the screen on Pan Gesture but also the view alpha should scale down to "zero", when it reaches to the bottom of the screen.
And vise versa, when I will drag the view upwards then the UIView alpha should scale down to "1"
But the problem is that the view's alpha is scaling down to "Zero" on panning half of the screen or sometimes when I drag the view slower.
Initially I have made the UIView background color to Black.
I need to scale down the alpha of the view gradually , any idea or suggestion will be helpful.
UIPanGestureRecognizer * panner = nil;
panner = [[UIPanGestureRecognizer alloc] initWithTarget: self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panner ];
[panner setDelegate:self];
[panner release];
CGRect frame = CGRectMake(0, 0, 320, 460);
self.dimmer = [[UIView alloc] initWithFrame:frame];
[self.dimmer setBackgroundColor:[UIColor blackColor]];
[self.view addSubview:dimmer];
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
static CGPoint lastPosition = {0};
CGPoint nowPosition; float alpha = 0.0;
float new_alpha = 0.0;
nowPosition = [sender translationInView: [self view]];
alpha = [dimmer alpha] -0.0037;
dimmer.alpha -=alpha;
}
I would look at the point on the screen you are currently at inside your handlePanGesture: find the percentage you are at on the view CGFloat percentage = nowPosition.y/self.view.frame.size.height; then set the alpha to that dimmer.alpha = 1.0 - percentage;. This way no matter where you are moving, you are setting the alpha to how close to the bottom you are.
You aren't scaling relative to your gesture; you're setting dimmer.alpha = 0.0037 every time handlePanGesture: executes, regardless of pan direction or distance.
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
static CGPoint lastPosition = {0};
CGPoint nowPosition;
float alpha = 0.0;
float new_alpha = 0.0; // Unused!!
nowPosition = [sender translationInView: [self view]]; // Unused!!
alpha = [dimmer alpha] - 0.0037;
dimmer.alpha -= alpha; // === dimmer.alpha = dimmer.alpha - (dimmer.alpha - 0.0037)
// === dimmer.alpha = 0.0037 !!!
}
A better implementation might look something like this:
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
CGPoint nowPosition = [sender translationInView: [self view]];
CGFloat alpha = dimmer.alpha - ([sender translationInView: [self view]].y)/320.0;
dimmer.alpha = MAX(0, MIN(1, alpha));
}

How can I drag a UICollectionViewCell from one UICollectionView to another UICollectionView?

I am making an iPad application. On one page of this application, there is a UICollectionView on the left-hand side and another UICollectionView on the right hand side. Each UICollectionView is one column wide.
The functionality I desire is as follows:
Each UICollectionViewCell on the left hand side should be able to be dragged to the UICollectionView on the right hand side. If this is not possible, then at least a UICollectionViewCell should be able to be dragged out of the left UICollectionView and then I'll handle having it appear in the righthand UICollectionView.
Is this functionality possible? If so, how would I go about implementing it?
You would want to attach a long press gesture recognizer to the common superview of both collectionViews. The drag operation is triggered by the long-press, and the entire transaction is handled within that recognizer. Because the pan gesture is used for scrolling the collectionviews, you will run into problems in trying to use the pan recognizer.
The key thing is the gesture recognizer needs to be attached the COMMON superview, and all points and rectangles are converted to the coordinate system of the superview.
This isn't the exact code (this moves from a CV to another view) but the process would be similar (NOTE: I have tried to strip out some code that would be irrelevant to your app, so I could have messed something up in the process -- but the concept holds):
- (void) processLongPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateChanged)
{
if (!dragView)
return;
CGPoint location = [sender locationInView:self.view];
CGPoint translation;
translation.x = location.x - dragViewStartLocation.x;
translation.y = location.y - dragViewStartLocation.y;
CGAffineTransform theTransform = dragView.transform;
theTransform.tx = translation.x;
theTransform.ty = translation.y;
dragView.transform = theTransform;
[self.view bringSubviewToFront:dragView];
return;
}
if (sender.state == UIGestureRecognizerStateBegan)
{
// if point gives a valid collectionView indexPath we are doing a long press on a picture item to begin a drag
// & drop operation.
CGPoint point = [sender locationInView:collectionView];
dragViewIndexPath = [collectionView indexPathForItemAtPoint:point];
if (dragViewIndexPath) // i.e., selected item in collection view.
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:dragViewIndexPath];
dragView = [cell.contentView viewWithTag:cell.tag];
[dragView removeFromSuperview];
[self.view addSubview:dragView];
dragView.center = [collectionView convertPoint:point toView:self.view];
dragViewStartLocation = dragView.center;
[self.view bringSubviewToFront:dragView];
}
return;
}
if (sender.state == UIGestureRecognizerStateEnded)
{
if (dragView)
{
dragView.center = CGPointMake(dragView.center.x + dragView.transform.tx, dragView.center.y + dragView.transform.ty);
CGAffineTransform theTransform = dragView.transform;
theTransform.tx = 0.0f;
theTransform.ty = 0.0f;
UIView *dropTarget = [self mapDisplayModeToReceiverView]; // get drop target
CGRect convertedTargetFrame = [self.view convertRect:dropTarget.frame fromView:dropTarget.superview];
if (CGRectContainsPoint(convertedTargetFrame, dragView.center)) // if so, then drop it.
{
ImageWithAttachedLabel *i = (ImageWithAttachedLabel *) dragView;
[speakStrings addObject:[i.labelText stringByAppendingString:#". "]];
UserData *uData = (UserData *)i.userDataObject;
UIImage *image = [[UIImage alloc] initWithData:uData.image];
CGRect newFrame = CGRectMake(0.0f, 0.0f, 140.0f, 140.0f);
ImageWithAttachedLabel *newImage = [[ImageWithAttachedLabel alloc] initWithFrame:newFrame withImage:image withLabel:uData.itemName];
newImage.tag = RECEIVERVIEW_MAGIC_NUMBER;
[self.view addSubview:newImage];
newImage.center = [receiverView convertPoint:dropTarget.center toView:self.view];
[UIView animateWithDuration:0.35f animations:^{ newImage.transform = CGAffineTransformMakeScale(1.15f, 1.15f); newImage.transform = CGAffineTransformIdentity; }
completion:^(BOOL finished) { if (finished)
{
[newImage removeFromSuperview];
newImage.frame = newFrame;
[dropTarget addSubview:newImage];
[dragView removeFromSuperview];
dragView=nil; }
}];
}
else
{
[dragView removeFromSuperview];
dragView = nil;
}
[self reloadData];
return;
}
}
There's no way to actually 'pass' a cell from a collection to the other, but you can do the following:
1) Once you detect that the user dragged a cell in the other collection, delete the cell from the first collection (let's call it Collection 1). You can maybe use a nice fade animation to make the cell disappear.
2) Add a cell to the second table with a nice animation (see the UICollectionView and UICollectionViewLayout methods and delegate methods for this).

How do I make an expand/contract transition between views on iOS?

I'm trying to make a transition animation in iOS where a view or view controller appears to expand to fill the whole screen, then contract back to its former position when done. I'm not sure what this type of transition is officially called, but you can see an example in the YouTube app for iPad. When you tap one of the search result thumbnails on the grid, it expands from the thumbnail, then contracts back into the thumbnail when you return to the search.
I'm interested in two aspects of this:
How would you make this effect when transitioning between one view and another? In other words, if view A takes up some area of the screen, how would you transition it to view B which takes up the whole screen, and vice versa?
How would you transition to a modal view this way? In other words, if UIViewController C is currently showing and contains view D which takes up part of the screen, how do you make it look like view D is turning into UIViewController E which is presented modally on top of C?
Edit: I'm adding a bounty to see if that gets this question more love.
Edit: I've got some source code that does this, and Anomie's idea works like a charm, with a few refinements. I had first tried animating the modal controller's view (E), but it didn't produce the effect of feeling like you're zooming into the screen, because it wasn't expanding all the stuff around the thumbnail view in (C). So then I tried animating the original controller's view (C), but the redrawing of it made for a jerky animation, and things like background textures did not zoom properly. So what I wound up doing is taking an image of the the original view controller (C) and zooming that inside the modal view (E). This method is substantially more complex than my original one, but it does look nice! I think it's how iOS must do its internal transitions as well. Anyway, here's the code, which I've written as a category on UIViewController.
UIViewController+Transitions.h:
#import <Foundation/Foundation.h>
#interface UIViewController (Transitions)
// make a transition that looks like a modal view
// is expanding from a subview
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController;
// make a transition that looks like the current modal view
// is shrinking into a subview
- (void)dismissModalViewControllerToView:(UIView *)view;
#end
UIViewController+Transitions.m:
#import "UIViewController+Transitions.h"
#implementation UIViewController (Transitions)
// capture a screen-sized image of the receiver
- (UIImageView *)imageViewFromScreen {
// make a bitmap copy of the screen
UIGraphicsBeginImageContextWithOptions(
[UIScreen mainScreen].bounds.size, YES,
[UIScreen mainScreen].scale);
// get the root layer
CALayer *layer = self.view.layer;
while(layer.superlayer) {
layer = layer.superlayer;
}
// render it into the bitmap
[layer renderInContext:UIGraphicsGetCurrentContext()];
// get the image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// close the context
UIGraphicsEndImageContext();
// make a view for the image
UIImageView *imageView =
[[[UIImageView alloc] initWithImage:image]
autorelease];
return(imageView);
}
// make a transform that causes the given subview to fill the screen
// (when applied to an image of the screen)
- (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView {
// get the root view
UIView *rootView = sourceView;
while (rootView.superview) rootView = rootView.superview;
// convert the source view's center and size into the coordinate
// system of the root view
CGRect sourceRect = [sourceView convertRect:sourceView.bounds toView:rootView];
CGPoint sourceCenter = CGPointMake(
CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));
CGSize sourceSize = sourceRect.size;
// get the size and position we're expanding it to
CGRect screenBounds = [UIScreen mainScreen].bounds;
CGPoint targetCenter = CGPointMake(
CGRectGetMidX(screenBounds),
CGRectGetMidY(screenBounds));
CGSize targetSize = screenBounds.size;
// scale so that the view fills the screen
CATransform3D t = CATransform3DIdentity;
CGFloat sourceAspect = sourceSize.width / sourceSize.height;
CGFloat targetAspect = targetSize.width / targetSize.height;
CGFloat scale = 1.0;
if (sourceAspect > targetAspect)
scale = targetSize.width / sourceSize.width;
else
scale = targetSize.height / sourceSize.height;
t = CATransform3DScale(t, scale, scale, 1.0);
// compensate for the status bar in the screen image
CGFloat statusBarAdjustment =
(([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)
/ scale);
// transform to center the view
t = CATransform3DTranslate(t,
(targetCenter.x - sourceCenter.x),
(targetCenter.y - sourceCenter.y) + statusBarAdjustment,
0.0);
return(t);
}
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController {
// get an image of the screen
UIImageView *imageView = [self imageViewFromScreen];
// insert it into the modal view's hierarchy
[self presentModalViewController:modalViewController animated:NO];
UIView *rootView = modalViewController.view;
while (rootView.superview) rootView = rootView.superview;
[rootView addSubview:imageView];
// make a transform that makes the source view fill the screen
CATransform3D t = [self transformToFillScreenWithSubview:sourceView];
// animate the transform
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = t;
} completion:^(BOOL finished) {
[imageView removeFromSuperview];
}];
}
- (void)dismissModalViewControllerToView:(UIView *)view {
// take a snapshot of the current screen
UIImageView *imageView = [self imageViewFromScreen];
// insert it into the root view
UIView *rootView = self.view;
while (rootView.superview) rootView = rootView.superview;
[rootView addSubview:imageView];
// make the subview initially fill the screen
imageView.layer.transform = [self transformToFillScreenWithSubview:view];
// remove the modal view
[self dismissModalViewControllerAnimated:NO];
// animate the screen shrinking back to normal
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = CATransform3DIdentity;
}
completion:^(BOOL finished) {
[imageView removeFromSuperview];
}];
}
#end
You might use it something like this in a UIViewController subclass:
#import "UIViewController+Transitions.h"
...
- (void)userDidTapThumbnail {
DetailViewController *detail =
[[DetailViewController alloc]
initWithNibName:nil bundle:nil];
[self expandView:thumbnailView toModalViewController:detail];
[detail release];
}
- (void)dismissModalViewControllerAnimated:(BOOL)animated {
if (([self.modalViewController isKindOfClass:[DetailViewController class]]) &&
(animated)) {
[self dismissModalViewControllerToView:thumbnailView];
}
else {
[super dismissModalViewControllerAnimated:animated];
}
}
Edit: Well, it turns out that doesn't really handle interface orientations other than portrait. So I had to switch to animating the transition in a UIWindow using a view controller to pass along the rotation. See the much more complicated version below:
UIViewController+Transitions.m:
#interface ContainerViewController : UIViewController { }
#end
#implementation ContainerViewController
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation {
return(YES);
}
#end
...
// get the screen size, compensating for orientation
- (CGSize)screenSize {
// get the size of the screen (swapping dimensions for other orientations)
CGSize size = [UIScreen mainScreen].bounds.size;
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
CGFloat width = size.width;
size.width = size.height;
size.height = width;
}
return(size);
}
// capture a screen-sized image of the receiver
- (UIImageView *)imageViewFromScreen {
// get the root layer
CALayer *layer = self.view.layer;
while(layer.superlayer) {
layer = layer.superlayer;
}
// get the size of the bitmap
CGSize size = [self screenSize];
// make a bitmap to copy the screen into
UIGraphicsBeginImageContextWithOptions(
size, YES,
[UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// compensate for orientation
if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
CGContextTranslateCTM(context, size.width, 0);
CGContextRotateCTM(context, M_PI_2);
}
else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
CGContextTranslateCTM(context, 0, size.height);
CGContextRotateCTM(context, - M_PI_2);
}
else if (self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
CGContextTranslateCTM(context, size.width, size.height);
CGContextRotateCTM(context, M_PI);
}
// render the layer into the bitmap
[layer renderInContext:context];
// get the image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// close the context
UIGraphicsEndImageContext();
// make a view for the image
UIImageView *imageView =
[[[UIImageView alloc] initWithImage:image]
autorelease];
// done
return(imageView);
}
// make a transform that causes the given subview to fill the screen
// (when applied to an image of the screen)
- (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView
includeStatusBar:(BOOL)includeStatusBar {
// get the root view
UIView *rootView = sourceView;
while (rootView.superview) rootView = rootView.superview;
// by default, zoom from the view's bounds
CGRect sourceRect = sourceView.bounds;
// convert the source view's center and size into the coordinate
// system of the root view
sourceRect = [sourceView convertRect:sourceRect toView:rootView];
CGPoint sourceCenter = CGPointMake(
CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));
CGSize sourceSize = sourceRect.size;
// get the size and position we're expanding it to
CGSize targetSize = [self screenSize];
CGPoint targetCenter = CGPointMake(
targetSize.width / 2.0,
targetSize.height / 2.0);
// scale so that the view fills the screen
CATransform3D t = CATransform3DIdentity;
CGFloat sourceAspect = sourceSize.width / sourceSize.height;
CGFloat targetAspect = targetSize.width / targetSize.height;
CGFloat scale = 1.0;
if (sourceAspect > targetAspect)
scale = targetSize.width / sourceSize.width;
else
scale = targetSize.height / sourceSize.height;
t = CATransform3DScale(t, scale, scale, 1.0);
// compensate for the status bar in the screen image
CGFloat statusBarAdjustment = includeStatusBar ?
(([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)
/ scale) : 0.0;
// transform to center the view
t = CATransform3DTranslate(t,
(targetCenter.x - sourceCenter.x),
(targetCenter.y - sourceCenter.y) + statusBarAdjustment,
0.0);
return(t);
}
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController {
// get an image of the screen
UIImageView *imageView = [self imageViewFromScreen];
// show the modal view
[self presentModalViewController:modalViewController animated:NO];
// make a window to display the transition on top of everything else
UIWindow *window =
[[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.hidden = NO;
window.backgroundColor = [UIColor blackColor];
// make a view controller to display the image in
ContainerViewController *vc = [[ContainerViewController alloc] init];
vc.wantsFullScreenLayout = YES;
// show the window
[window setRootViewController:vc];
[window makeKeyAndVisible];
// add the image to the window
[vc.view addSubview:imageView];
// make a transform that makes the source view fill the screen
CATransform3D t = [self
transformToFillScreenWithSubview:sourceView
includeStatusBar:(! modalViewController.wantsFullScreenLayout)];
// animate the transform
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = t;
} completion:^(BOOL finished) {
// we're going to crossfade, so change the background to clear
window.backgroundColor = [UIColor clearColor];
// do a little crossfade
[UIView animateWithDuration:0.25
animations:^(void) {
imageView.alpha = 0.0;
}
completion:^(BOOL finished) {
window.hidden = YES;
[window release];
[vc release];
}];
}];
}
- (void)dismissModalViewControllerToView:(UIView *)view {
// temporarily remove the modal dialog so we can get an accurate screenshot
// with orientation applied
UIViewController *modalViewController = [self.modalViewController retain];
[self dismissModalViewControllerAnimated:NO];
// capture the screen
UIImageView *imageView = [self imageViewFromScreen];
// put the modal view controller back
[self presentModalViewController:modalViewController animated:NO];
[modalViewController release];
// make a window to display the transition on top of everything else
UIWindow *window =
[[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.hidden = NO;
window.backgroundColor = [UIColor clearColor];
// make a view controller to display the image in
ContainerViewController *vc = [[ContainerViewController alloc] init];
vc.wantsFullScreenLayout = YES;
// show the window
[window setRootViewController:vc];
[window makeKeyAndVisible];
// add the image to the window
[vc.view addSubview:imageView];
// make the subview initially fill the screen
imageView.layer.transform = [self
transformToFillScreenWithSubview:view
includeStatusBar:(! self.modalViewController.wantsFullScreenLayout)];
// animate a little crossfade
imageView.alpha = 0.0;
[UIView animateWithDuration:0.15
animations:^(void) {
imageView.alpha = 1.0;
}
completion:^(BOOL finished) {
// remove the modal view
[self dismissModalViewControllerAnimated:NO];
// set the background so the real screen won't show through
window.backgroundColor = [UIColor blackColor];
// animate the screen shrinking back to normal
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = CATransform3DIdentity;
}
completion:^(BOOL finished) {
// hide the transition stuff
window.hidden = YES;
[window release];
[vc release];
}];
}];
}
Whew! But now it looks just about like Apple's version without using any restricted APIs. Also, it works even if the orientation changes while the modal view is in front.
Making the effect is simple. You take the full-sized view, initialize its transform and center to position it on top of the thumbnail, add it to the appropriate superview, and then in an animation block reset the transform and center to position it in the final position. To dismiss the view, just do the opposite: in an animation block set transform and center to position it on top of the thumbnail, and then remove it completely in the completion block.
Note that trying to zoom from a point (i.e. a rectangle with 0 width and 0 height) will screw things up. If you're wanting to do that, zoom from a rectangle with width/height something like 0.00001 instead.
One way would be to do the same as in #1, and then call presentModalViewController:animated: with animated NO to present the actual view controller when the animation is complete (which, if done right, would result in no visible difference due to the presentModalViewController:animated: call). And dismissModalViewControllerAnimated: with NO followed by the same as in #1 to dismiss.
Or you could manipulate the modal view controller's view directly as in #1, and accept that parentViewController, interfaceOrientation, and some other stuff just won't work right in the modal view controller since Apple doesn't support us creating our own container view controllers.
After watching the Youtube iPad animation, I figured out that it's just an illusion. Let's say that there's a SearchViewController for the search results, and a DetailViewController for the video itself, and the additional info of the video.
DetailViewController has a method like - (id)initWithFullscreen which starts the view controller using the full screen space with the video.
So the sequence goes like this:
SearchViewController presents its results.
User clicks on a video.
DetailViewController is created with initWithFullscreen, but not presented
The "Zoom in" animation begins. (Notice that we are still on the SearchViewController, and this animation is just a simple View animation)
The "Zoom in" animation ends, presents the DetailViewController with animated:NO (as Anomie mentioned).
The DetailViewController is now presented, and using full space.
It doesn't seem that the youtube app is doing anything fancier, the give-away was that the "Zoom in" animation zooms to a black square, before presenting the full video.

Transferring touches to copy of UIImageView

I've been a long journey trying to drag and drop UIImageViews from a UITableView to another view.
I have everything working to a point. I subclass UIPanGestureRecognizer and attach this custom recognizer to all the UIImageViews I load into the table view.
I then override the recognizer's touchesMoved method to, among other things, add a temporary view onto the applications key window, then add a copy of the selected UIImageView onto this temporary view, as follows:
UIView *dragAndDropView = [[UIView alloc] initWithFrame: [[UIScreen mainScreen] applicationFrame]];
dragAndDropView.backgroundColor = [UIColor whiteColor];
[[UIApplication sharedApplication].keyWindow addSubview: dragAndDropView];
ElementImageView * sourceImageView = (ElementImageView*)self.view;
ElementImageView *newImageView = [[ElementImageView alloc] initWithImage:sourceImageView.image];
newImageView.userInteractionEnabled = YES;
newImageView.frame = [self.view convertRect:sourceImageView.frame toView:dragAndDropView];
[dragAndDropView addSubview:newImageView];
As a result of this code, I do indeed get a copy of the source image view on top of my dragAndDropView (I can tell because I set the backgroundcolor of the dragAndDropView to white). And if I lift my finger and touch the copied image view again, I can of course drag it around the screen as I wish. But I can't find a way to seamlessly transfer the existing touch to the new object.
Any ideas?
I think you could do the following on your UIPanGestureRecognizer handler:
-(void) handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
// the initial position
initialPanPositionX = self.transform.tx;
initialPanPositionY = self.transform.ty;
// create a copy of the imageview
panImage = [[UIImageView alloc] initWithImage:imageView.image];
panImage.frame = imageView.frame;
[self.view addSubview:panImage];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
if (!startedMoving) {
startedMoving = YES;
[UIView beginAnimations:nil context:NULL]; {
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
[UIView setAnimationDuration:0.2f];
// an effect to increase a copy a little bit when start dragging
CGAffineTransform zt = CGAffineTransformScale(CGAffineTransformIdentity, 1.2, 1.2);
panImage.bounds = CGRectApplyAffineTransform(panImage.bounds, zt);
}
[UIView commitAnimations];
}
// translation
CGPoint point = [recognizer translationInView:self];
panImage.transform = CGAffineTransformMakeTranslation(initialPanPositionX + point.x, initialPanPositionY + point.y);
}
else if (recognizer.state == UIGestureRecognizerStateEnded ) {
startedMoving = NO;
// drop the imageView if it's inside the droppable view
[panImage release], panImage = nil;
}
}
Hope it helps.

Resources