How to flip a UIView around the x-axis while simultaneously switching subviews - objective-c

This question has been asked before but in a slightly different way and I was unable to get any of the answers to work the way I wanted, so I am hoping somebody with great Core Animation skills can help me out.
I have a set of cards on a table. As the user swipes up or down the set of cards move up and down the table. There are 4 cards visible on the screen at any given time, but only the second card is showing its face. As the user swipes the second card flips back onto its face and the next card (depending on the swipe direction) lands in it's place showing its face.
I have set up my card view class like this:
#interface WLCard : UIView {
UIView *_frontView;
UIView *_backView;
BOOL flipped;
}
And I have tried flipping the card using this piece of code:
- (void) flipCard {
[self.flipTimer invalidate];
if (flipped){
return;
}
id animationsBlock = ^{
self.backView.alpha = 1.0f;
self.frontView.alpha = 0.0f;
[self bringSubviewToFront:self.frontView];
flipped = YES;
CALayer *layer = self.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / 500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 1.0f, 0.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;
};
[UIView animateWithDuration:0.25
delay:0.0
options: UIViewAnimationCurveEaseInOut
animations:animationsBlock
completion:nil];
}
This code works but it has the following problems with it that I can't seem to figure out:
Only half of the card across the x-axis is animated.
Once flipped, the face of the card is upside down and mirrored.
Once I've flipped the card I cannot get the animation to ever run again. In other words, I can run the animation block as many times as I want, but only the first time will animate. The subsequent times I try to animate lead to just a fade in and out between the subviews.
Also, bear in mind that I need to be able to interact with the face of the card. i.e. it has buttons on it.
If anybody has run into these issues it would be great to see your solutions. Even better would be to add a perspective transform to the animation to give it that extra bit of realism.

This turned out to be way simpler than I thought and I didn't have to use any CoreAnimation libraries to achieve the effect. Thanks to #Aaron Hayman for the clue. I used transitionWithView:duration:options:animations:completion
My implementation inside the container view:
[UIView transitionWithView:self
duration:0.2
options:UIViewAnimationOptionTransitionFlipFromBottom
animations: ^{
[self.backView removeFromSuperview];
[self addSubview:self.frontView];
}
completion:NULL];
The trick was the UIViewAnimationOptionTransitionFlipFromBottom option. Incidentally, Apple has this exact bit of code in their documentation. You can also add other animations to the block like resizing and moving.

Ok, this won't be a complete solution but I'll point out some things that might be helpful. I'm not a Core-Animation guru but I have done a few 3D rotations in my program.
First, there is no 'back' to a view. So if you rotate something by M_PI (180 degrees) you're going to be looking at that view as though from the back (which is why it's upside down/mirrored).
I'm not sure what you mean by:
Only half of the card across the x-axis is animated.
But, it it might help to consider your anchor point (the point at which the rotation occurs). It's usually in the center, but often you need it to be otherwise. Note that anchor points are expressed as a proportion (percentage / 100)...so the values are 0 - 1.0f. You only need to set it once (unless you need it to change). Here's how you access the anchor point:
layer.anchorPoint = CGPointMake(0.5f, 0.5f) //This is center
The reason the animation only ever runs once is because transforms are absolute, not cumulative. Consider that you're always starting with the identity transform and then modifying that, and it'll make sense...but basically, no animation occurs because there's nothing to animate the second time (the view is already in the state you're requesting it to be in).
If you're animating from one view to another (and you can't use [UIView transitionWithView:duration:options:animations:completion:];) you'l have to use a two-stage animation. In the first stage of the animation, for the 'card' that is being flipped to backside, you'll rotate the view-to-disappear 'up/down/whatever' to M_PI_2 (at which point it will be 'gone', or not visible, because of it's rotation). And in the second stage, you're rotate the backside-of-view-to-disappear to 0 (which should be the identity transform...aka, the view's normal state). In addition, you'll have to do the exact opposite for the 'card' that is appearing (to frontside). You can do this by implementing another [UIView animateWithDuration:...] in the completion block of the first one. I'll warn you though, doing this can get a little bit complicated. Especially since you're wanting views to have a 'backside', which will basically require animating 4 views (the view-to-disappear, the view-to-appear, backside-of-view-to-disappear, and the backside-of-view-to-appear). Finally, in the completion block of the second animation you can do some cleanup (reset view that are rotated and make their alpha 0.0f, etc...).
I know this is complicated, so you might want read some tutorial on Core-Animation.

#Aaron has some good info that you should read.
The simplest solution is to use a CATransformLayer that will allow you to place other CALayer's inside and maintain their 3D hierarchy.
For example to create a "Card" that has a front and back you could do something like this:
CATransformLayer *cardContainer = [CATransformLayer layer];
cardContainer.frame = // some frame;
CALayer *cardFront = [CALayer layer];
cardFront.frame = cardContainer.bounds;
cardFront.zPosition = 2; // Higher than the zPosition of the back of the card
cardFront.contents = (id)[UIImage imageNamed:#"cardFront"].CGImage;
[cardContainer addSublayer:cardFront];
CALayer *cardBack = [CALayer layer];
cardBack.frame = cardContainer.bounds;
cardBack.zPosition = 1;
cardBack.contents = (id)[UIImage imageNamed:#"cardBack"].CGImage; // You may need to mirror this image
[cardContainer addSublayer:cardBack];
With this you can now apply your transform to cardContainer and have a flipping card.

#Paul.s
I followed your approach with card container but when i applt the rotation animation on card container only one half of the first card rotates around itself and finally the whole view appears.Each time one side is missing in the animation

Based on Paul.s this is updated for Swift 3 and will flip a card diagonally:
func createLayers(){
transformationLayer = CATransformLayer(layer: CALayer())
transformationLayer.frame = CGRect(x: 15, y: 100, width: view.frame.width - 30, height: view.frame.width - 30)
let black = CALayer()
black.zPosition = 2
black.frame = transformationLayer.bounds
black.backgroundColor = UIColor.black.cgColor
transformationLayer.addSublayer(black)
let blue = CALayer()
blue.frame = transformationLayer.bounds
blue.zPosition = 1
blue.backgroundColor = UIColor.blue.cgColor
transformationLayer.addSublayer(blue)
let tgr = UITapGestureRecognizer(target: self, action: #selector(recTap))
view.addGestureRecognizer(tgr)
view.layer.addSublayer(transformationLayer)
}
Animate a full 360 but since the layers have different zPositions the different 'sides' of the layers will show
func recTap(){
let animation = CABasicAnimation(keyPath: "transform")
animation.delegate = self
animation.duration = 2.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(Float.pi), 1, -1, 0))
transformationLayer.add(animation, forKey: "arbitrarykey")
}

Related

NSScrollView: fade in a top-border like Messages.app

What I Want to Do:
In Messages.app on OS 10.10, when you scroll the left-most pane (the list of conversations) upwards, a nice horizontal line fades in over about 0.5 seconds. When you scroll back down, the line fades back out.
What I Have:
I am trying to achieve this effect in my own app and I've gotten very close. I subclassed NSScrollView and have done the following:
- (void) awakeFromNib
{
_topBorderLayer = [[CALayer alloc] init];
CGColorRef bgColor = CGColorCreateGenericGray(0.8, 1.0f);
_topBorderLayer.backgroundColor = bgColor;
CGColorRelease(bgColor);
_topBorderLayer.frame = CGRectMake(0.0f, 0.0f, self.bounds.size.width, 1.0f);
_topBorderLayer.autoresizingMask = kCALayerWidthSizable;
_topBorderLayer.zPosition = 1000000000;
_fadeInAnimation = [[CABasicAnimation animationWithKeyPath:#"opacity"] retain];
_fadeInAnimation.duration = 0.6f;
_fadeInAnimation.fromValue = #0;
_fadeInAnimation.toValue = #1;
_fadeInAnimation.removedOnCompletion = YES;
_fadeInAnimation.fillMode = kCAFillModeBoth;
[self.layer insertSublayer:_topBorderLayer atIndex:0];
}
- (void) layoutSublayersOfLayer:(CALayer *)layer
{
NSPoint origin = [self.contentView documentVisibleRect].origin;
// 10 is a fudge factor for blank space above first row's actual content
if (origin.y > 10)
{
if (!_topBorderIsShowing)
{
_topBorderIsShowing = YES;
[_topBorderLayer addAnimation:_fadeInAnimation forKey:nil];
_topBorderLayer.opacity = 1.0f;
}
}
else
{
if (!_topBorderIsShowing)
{
_topBorderIsShowing = NO;
// Fade out animation here; omitted for brevity
}
}
}
The Problem
The "border" sublayer that I add is not drawing over top of all other content in the ScrollView, so that we end up with this:
The frames around the image, textfield and checkbox in this row of my outlineView are "overdrawing" my border layer.
What Causes This
I THINK this is because the scrollView is contained inside an NSVisualEffectView that has Vibrancy enabled. The reason I think this is that if I change the color of my "border" sublayer to 100% black, this issue disappears. Likewise, if I turn on "Reduce Transparency" in OS X's System Preferences > Accessibility, the issue disappears.
I think the Vibrancy compositing is taking my grey border sublayer and the layers that represent each of those components in the outlineView row and mucking up the colors.
So... how do I stop that for a single layer? I've tried all sorts of things to overcome this. I feel like I'm 99% of the way to a solid implementation, but can't fix this last issue. Can anyone help?
NB:
I am aware that it's dangerous to muck directly with layers in a layer-backed environment. Apple's docs make it clear that we can't change certain properties of a view's layer if we're using layer-backing. However: adding and removing sublayers (as I am) is not a prohibited action.
Update:
This answer, while it works, causes problems if you're using AutoLayout. You'll start to get warnings that the scrollView still needs update after calling Layout because something dirtied the layout in the middle of updating. I have not been able to find a workaround for that, yet.
Original solution:
Easiest way to fix the problem is just to inset the contentView by the height of the border sublayer with this:
- (void) tile
{
id contentView = [self contentView];
[super tile];
[contentView setFrame:NSInsetRect([contentView frame], 0.0, 1.0)];
}
Should have thought of it hours ago. Works great. I'll leave the question for anyone who might be looking to implement these nice fading-borders.

How to get object position and size in UIView objective c

I put UIImageView in my Scene from Object library, and give it an image and defined OUTLET in .h file. Now I want to check its coordinates, or center point, or frame X,Y,Width,Height.
I am using
This
CGRect newFrameSize = CGRectMake(recycleBin.frame.origin.x, recycleBin.frame.origin.y,
recycleBin.frame.size.width, recycleBin.frame.size.height);
or
CGRect newFrameSize = recycleBin.frame;
by using this
NSLog(#"%#", NSStringFromCGRect(newFrameSize));
gives same result that is
2013-01-16 21:42:25.101 xyzapp[6474:c07] {{0, 0}, {0, 0}}
I want its actual position and size when viewcontroller loaded, so when user click on image view it will fadeout by zoom-in towards users and will disappear, and when user tap on reset button, it fadein and zoom-in back to original form (reverse to the previous animation).
Also give me hint, how to perform this animation on UIImageView or any button or label. Thx
Unfortunately, you can't check an item's actual frame as set in IB in -viewDidLoad. The earliest you can check it (that I've found) is by overriding -viewDidAppear:. But, since -viewDidAppear: could be called multiple times throughout the life of the view, you need to make sure you're not saving the frame it's in the modified state.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if(savedFrame == CGRectZero) {
savedFrame = self.recycleBin.frame;
NSLog(#"Frame: %#", NSStringFromCGRect(savedFrame));
}
}
Where savedFrame is a member variable (or you could make it a property).
From the description of the animation you're wanting, it sounds like adjusting the frame isn't the way to go about it. It sounds like you're wanting to get the effect of the view stretching and fading out (and the reverse when being reset)? If so, some code like this might be more so what you're looking for...
Fade out:
float animationDuration = 2.0f; // Duration of animation in seconds
float zoomScale = 3.0f; // How much to zoom in duration the animation
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(zoomScale, zoomScale);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 0; // Make fully transparent
}];
And then, to reset the view:
float animationDuration = 2.0f; // Duration of animation in seconds
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(1.0f, 1.0f);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 1.0; // Make fully opaque
}];
You can play around with the numbers to see if you get the effects you desire. Most animations in iOS are actually extremely simple to do. This code would work for any UIView subclass.
It sounds as if your IBOutlet is not attached to your class.
Open up your view controller header file (if that is where you property declaration is) and look beside the declaration:
Notice how on the first IBOutlet, the circle (to the left of the line number) is filled in. This means that it is connected to your scene. However, the second one is not (the circle is not filled in).

iOS Core Animation Speed Up

I am trying to replace one complex view with another complex view using different animations, like moving to left/right/top/bottom. First and second views contains 30-40 subviews (buttons). My code is like this:
oldView.alpha = 1;
newView.alpha = 0;
oldView.frame = CGRectMake(0, 0, width, height);
newView.frame = CGRectMake(0, -height, width, height);
// begin animation
// setting duration 0.3
// ...
oldView.alpha = 0;
newView.alpha = 1;
oldView.frame = CGRectMake(0, height, width, height);
newView.frame = CGRectMake(0, 0, width, height);
// commit animation
// ...
It works nice on iPhone Simulator and on iPhone 4S, but it lags on iPhone 4. By lag I mean 12-15 FPS.
How can I speed up this animation?
Should I use center property instead of frame?
Should I render my views to UIImageView's and animate them?
Should I layout my views in UIScrollView and call scrollRectToVisible:animated:?
And please, explain me why my animation code so slow? When my two views contains 10-20 buttons - there are no problems with speed...
You will need to post more code as setting a new frame shouldn't be too much of a performance hit. There are however things you can do with your view's layer or in the drawRect: that would slow drawing and animation down considerably.
For instance:
myView.layer.shadowOffset = CGSizeMake(0.0, 10.0);
myView.layer.shadowRadius = 10;
myView.layer.shadowOpacity = 0.40;
Which adds a nice drop shadow to the view will kill the animation. So in this instance I turn off the shadows when animating and back on again when done.
Without seeing more of your code it is impossible to say where the problem lies.
EDIT:
To speed up the rendering of shadows you can set the shouldRasterize property to YES. This forces the layer to create a bitmap of the shadow rather than trying to redraw it every frame.
Try one of the following:
change center instead of frame
use transform property instead of frame. oldView.transform = CGAffineTransformMakeTranslation(0, height);
Just to add, if you are using explicit animations yet still setting animatable properties, you'll want to disable implicit animations by using:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

Simple multi-touch drawing application on iOS: Too slow (because drawRect() not additive ?)

I am using Quartz 2D to make a simple multi-touch drawing iPad game. The game requires me to draw a new stroke at the finger position every 1/30th of a second.
As far as I know, there is basically no way to get drawRect() to not clear the context every time it is called (self.clearsContextBeforeDrawing = NO; does not work), so my solution was to create a back buffer bitmap (or layer, I can use both), draw every new small stroke into that back buffer every iteration for each finger, and then copy the buffer into the screen every call to drawRect(). In other words:
backlayer = CGLayerCreateWithContext(context, CGSizeMake(W, H), NULL);
offctx = CGLayerGetContext (backlayer);
and then
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
//code here to draw small strokes from old finger position to new one
CGContextDrawLayerInRect(context, [self bounds], backlayer);
}
This worked without problems while I was testing on my iPad 2, but yesterday I noticed that this same code runs much slower on the new iPad 3. The performance is abysmal, slowing my game down from 30FPS all the way to about 5 or so, probably due to the larger, retina display. I have the same problem if I use a separate CGBitmapContext that I create, and then every iteration I create an ImageRef from it and paint it with CGContextDrawImage.
What approach could I take to address this? It seems like I must redraw everything every iteration since it's not good enough to even pass a small rectange to drawRect of what has changed (since every iteration there would need to be several rectangles for each finger)
Thank you
I managed to resolve this as follows:
I create a new UIView subclass header and implementation files:
#interface fingerView : UIView {
}
Then in my main view, in header I declare 5 of these views:
fingerView* fview[5];
and in my main view implementation I create 5 views of this instance, one for each finger separately. Also, must make sure to make them, enable multitouch for each of them, and make sure that clearsContextBeforeDrawing is set to NO, as we will be updating tiny rects in each of them at a time, and we dont want the system to clear our work.
for(int i=0;i<5;i++) {
fview[i] = [[pView alloc] initWithFrame:topFrame];
[self addSubview: fview[i]];
[self sendSubviewToBack: fview[i]];
fview[i].opaque= NO;
fview[i].clearsContextBeforeDrawing = NO;
fview[i].multipleTouchEnabled = YES;
}
Now inside every finger view keep a large array (i use a simple array, say 10,000 long) of x and y positions that the finger had drawn on. Whenever a finger moves, the main view detects it, and calls a [fview[i] updatePos(newx, newy)], and crucially, we will command the view to only update a tiny potion of itself around these coordinates:
[fview[i] setNeedsDisplayInRect: fingerRect];
where fingerRect is a small rect centered at (newx, newy). Inside the drawRect method for every finger view,
- (void)drawRect:(CGRect)rect
{
if (movep==0) return;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, r, g, b, 1);
CGContextSetLineWidth(context, linewidth);
//paint finger
CGContextBeginPath(context);
CGFloat slack= 15;
CGFloat minx= CGRectGetMinX(rect)-slack;
CGFloat maxx= CGRectGetMaxX(rect)+slack;
CGFloat miny= CGRectGetMinY(rect)-slack;
CGFloat maxy= CGRectGetMaxY(rect)+slack;
bool drawing = NO;
for(int i=0;i<movep;i++) {
CGFloat xx= x[i];
CGFloat yy= y[i];
if(xx>minx && xx<maxx && yy>miny && yy<maxy) {
if(drawing) {
// continue line
CGContextAddLineToPoint(context, xx, yy);
CGContextMoveToPoint(context, xx, yy);
} else {
// start drawing
CGContextMoveToPoint(context, xx, yy);
drawing= YES;
}
} else {
drawing= NO;
}
}
CGContextStrokePath(context);
and also, as I mentioned
- (void)updatePos: (CGFloat)xnew: (CGFloat) ynew
{
x[movep]= xnew;
y[movep]= ynew;
movep= movep+1;
Hopefully you can figure out how this works. Every view will look into this rectangle that has been modified, and checks all finger positions that went around that rect, and only draws those. This will come down to very few strokes, and so the entire code runs very fast.
The lesson overall is that UIViews are extremely optimized. As much as possible, try to make a whole bunch of them, update them only locally if at all, and let Apple's magic blend it all together.

taking image snapshot of CATiledLayer-backed view in UIScrollView

I've got a custom map view which is made of a UIScrollView. The scroll view's subview is backed by a CATiledLayer. Everything works great here. Panning & zooming loads up new map tiles and everything performs well.
What I want to do is capture frames of video of animations to this scroll view. Essentially, I want to create a video of animated changes to the scroll view's contentOffset and zoomScale.
I know that the concept is sound as I can get the private API function UIGetScreenImage() to capture the app's screen at, say, 10fps, combine these images, and I get playback animations that are smooth and have the timing curves used by the scroll view animations.
My problem, of course, is that I can't use the private API. Going through the alternatives outlined by Apple here leaves me with pretty much one supposedly valid option: asking a CALayer to renderInContext and taking a UIGraphicsGetImageFromCurrentImageContext() from that.
This just doesn't seem to work with CATiledLayer-backed views, though. A blocky, un-zoomed image is what is captured, as if the higher-resolution tiles never load. This somewhat makes sense given that CATiledLayer draws in background threads for performance and calling renderInContext from the main thread might not catch these updates. The result is similar even if I render the tiled layer's presentationLayer as well.
Is there an Apple-sanctioned way of capturing an image of a CATiledLayer-backed view during the course of the containing scroll view's animations? Or at any point, for that matter?
BTW, this is doable if you properly implement renderLayer:inContext: in your CATiledLayer-backed view.
I did a quick test, and using renderInContext: on a view wrapping the scroll view seemed to work. Have you tried that?
This code works for me.
- (UIImage *)snapshotImageWithView:(CCTiledImageScrollView *)view
{
// Try our best to approximate the best tile set zoom scale to use
CGFloat tileScale;
if (view.zoomScale >= 0.5) {
tileScale = 2.0;
}
else if (view.zoomScale >= 0.25) {
tileScale = 1.0;
}
else {
tileScale = 0.5;
}
// Calculate the context translation based on how far zoomed in or out.
CGFloat translationX = -view.contentOffset.x;
CGFloat translationY = -view.contentOffset.y;
if (view.contentSize.width < CGRectGetWidth(view.bounds)) {
CGFloat deltaX = (CGRectGetWidth(view.bounds) - view.contentSize.width) / 2.0;
translationX += deltaX;
}
if (view.contentSize.height < CGRectGetHeight(view.bounds)) {
CGFloat deltaY = (CGRectGetHeight(view.bounds) - view.contentSize.height) / 2.0;
translationY += deltaY;
}
// Pass the tileScale to the context because that will be the scale used in drawRect by your CATiledLayer backed UIView
UIGraphicsBeginImageContextWithOptions(CGSizeMake(CGRectGetWidth(view.bounds) / view.zoomScale, CGRectGetHeight(view.bounds) / view.zoomScale), NO, tileScale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, translationX / view.zoomScale, translationY / view.zoomScale);
// The zoomView is a subview of UIScrollView. The CATiledLayer backed UIView is a subview of the zoomView.
[view.zoomView.layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Full sample code found here: https://github.com/gortega56/CCCanvasView