How to flip a card while scaling it in Swift - ios

I'm trying to take a card UIView object, which has both back and front image subviews in it, and flip it while scaling it larger until mid flip and then scaling it back down. I have it working with just the flipping part, using UIView.transition(with:duration:options:animations:completion:), but this doesn't seem to be the correct solution, as all animations in the block only occur halfway through the full animation, which makes sense, since that's the point the views need to be added/removed.
I'm guessing I need to drop down to a lower level here, but I'm not that familiar with animations beyond the UIView layer. Any suggestions on how to add this scaling functionality to the card flip?

I'd put the both sides of the card under the layers of the view, you can do so by setting the contents of a CALayer. Then you apply the CABasicAnimation on the sublayers.
This is a simple demo which should run on the playground, without scaling implemented, you can modify the matrix by CATransform3DRotate(t:angle:x:y:z:) yourself.
var t = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
t.m34 = 0.001 // set a 3D perspective to simulate real flipping
let anim = CABasicAnimation(keyPath: "transform")
anim.byValue = NSValue(caTransform3D: t)
anim.duration = 3
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
anim.fillMode = kCAFillModeForwards
anim.isRemovedOnCompletion = true
let cardContainer = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 100))
cardContainer.backgroundColor = UIColor.clear
// layer1: front side of the card
let layer1 = CALayer()
layer1.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
layer1.backgroundColor = UIColor.red.cgColor
layer1.isDoubleSided = false
// layer2: back side of the card
let layer2 = CALayer()
layer2.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
layer2.backgroundColor = UIColor.blue.cgColor
layer2.isDoubleSided = false
layer2.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
cardContainer.layer.addSublayer(layer1)
cardContainer.layer.addSublayer(layer2)
layer1.add(anim, forKey: "anim1")
layer2.add(anim, forKey: "anim2")

Related

How to Bend or distortion a UILabel Text from one side

I have used attributed String and Libraries but unable to find any proper solution for Label Text effect.
Can Anyone please suggest me how i can achieve this functionality.?
Thanks
Adapting the code from How do I apply a perspective transform to a UIView?, here is some sample Swift 4 code that applies perspective to a label giving a result similar to your image.
let pview = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 300))
pview.backgroundColor = .black
let plabel = UILabel(frame: CGRect(x: 0, y: 110, width: 250, height: 75))
pview.addSubview(plabel)
plabel.text = " ND420 "
plabel.textAlignment = .center
plabel.font = UIFont.boldSystemFont(ofSize: 72)
plabel.textColor = .orange
plabel.backgroundColor = .red
plabel.sizeToFit()
let layer = plabel.layer
var transform = CATransform3DIdentity
transform.m34 = 1.0 / -200
transform = CATransform3DRotate(transform, -45 * CGFloat.pi / 180, 0, 1, 0)
layer.transform = transform
Run this in the playground and view pview.
Play with the value being assigned to transform.m34 and the rotation angle in CATransform3DRotate. A negative rotation angle (as shown above) makes the left side smaller and the right side larger. A positive angle does the opposite.

CALayer Mask Animation not disappearing

Ive set up a splash screen like twitters opening, and it runs fine, but once the animation is complete, the mask returns to normal and doesn't disappear after widening. Here is the code for the mask & animation:
override func viewDidLoad() {
super.viewDidLoad()
//setting up the arm mask
imageView.image = UIImage(named: "placeholderbg.png")
self.mask = CALayer()
self.mask?.contents = UIImage(named: "arm.png")?.cgImage
self.mask?.bounds = CGRect(x: 0, y: 0, width: 120, height: 100)
self.mask?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.mask?.position = CGPoint(x: view.frame.size.width/2, y: view.frame.size.height/2)
imageView.layer.mask = mask
//animation of the mask
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
let keyFrameAnimation = CAKeyframeAnimation(keyPath: "bounds")
keyFrameAnimation.duration = 1
keyFrameAnimation.timingFunctions =
[CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut),
CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)]
let initialBounds = NSValue(cgRect: self.mask!.bounds)
let secondBounds = NSValue(cgRect: CGRect(x: 0, y: 0, width: 110, height: 90))
let finalBounds = NSValue(cgRect: CGRect(x: 0, y: 0, width: 6000, height: 5000))
keyFrameAnimation.values = [initialBounds, secondBounds, finalBounds]
keyFrameAnimation.keyTimes = [0, 0.3, 1]
self.mask!.add(keyFrameAnimation, forKey: "bounds")
}
}
image of what the mask returns to after animation is complete instead of disappearing
As with any animation, when the animation is over it is removed from the render tree and the "true" state of affairs is revealed. It is up to you to set the animated layer's true state to its final state, so that when the animation reaches its final frame and is removed, that final frame is matched by the layer's true state.
You are not doing that. You add the bounds animation to the mask layer, but you do not change the mask's real bounds. So when the animation ends, the mask layer appears to jump back to its initial bounds — because you never changed them.
As for the "disappear" part, it's hard to tell what you mean, but it's likely a different matter; I don't see anything in your code that would make the mask "disappear" so I'm not clear on what you expect. If you want something new to happen at the end of the animation, you need to attach a completion function to it.

Playground code: AffineTransform and AnchorPoint don't work as I want

I want to shrink a triangle downward as below:
But the triangle shrink upward as below:
The transform code is below:
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
I tried to use anchorPoint but I can't scceed.
//: Playground - noun: a place where people can play
import UIKit
let view = UIView(frame: CGRectMake(0, 0, 200, 150))
let layer = CAShapeLayer()
let triangle = UIBezierPath()
triangle.moveToPoint (CGPointMake( 50, 150))
triangle.addLineToPoint(CGPointMake(100, 50))
triangle.addLineToPoint(CGPointMake(150, 150))
triangle.closePath()
layer.path = triangle.CGPath
layer.strokeColor = UIColor.blueColor().CGColor
layer.lineWidth = 3.0
view.layer.addSublayer(layer)
view
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
view
Anish gave me a advice but doesn't work well:
layer.setAffineTransform(CGAffineTransformMakeScale(2.0, 0.5))
Transforms operate around the layer's anchorPoint, which by default is at the center of the layer. Thus, you shrink by collapsing inwards, not by collapsing downwards.
If you want to shrink downward, you will need to scale down and change the view's position (or use a translate transform in addition to your scale transform), or change the layer's anchorPoint before performing the transform.
Another serious problem with your code is that your layer has no assigned size. This causes the results of your transform to be very misleading.
I ran this version of your code and got exactly the results you are asking for. I have put a star next to the key lines that I added. (Note that this is Swift 3; you really need to step up to the plate and update here.)
let view = UIView(frame:CGRect(x: 0, y: 0, width: 200, height: 150))
let layer = CAShapeLayer()
layer.frame = view.layer.bounds // *
let triangle = UIBezierPath()
triangle.move(to: CGPoint(x: 50, y: 150))
triangle.addLine(to: CGPoint(x: 100, y: 50))
triangle.addLine(to: CGPoint(x: 150, y: 150))
triangle.close()
layer.path = triangle.cgPath
layer.strokeColor = UIColor.blue.cgColor
layer.lineWidth = 3
view.layer.addSublayer(layer)
view
layer.anchorPoint = CGPoint(x: 0.5, y: 0) // *
layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 0.5))
view
Before:
After:

Using drawRect to draw & animate two graphs. A background graph, and a foreground graph

I am using drawRect to draw a pretty simple shape (dark blue in the image below).
I'd like this to animate from the left to the right, so that it grows. The caveat here is I need there to be a "max" background in gray, as seen in the top part of the image.
Right now, I'm simulating this animation by overlaying a white view, and then animating the size of it, so that it looks like the blue is animating to the right. While this works... I need the background gray shape to always be there. With my overlayed white view, this just doesn't work.
Here's the code for drawing the "current code" version:
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 0, self.bounds.height - 6)
CGContextAddLineToPoint(context, self.bounds.width, 0)
CGContextAddLineToPoint(context, self.bounds.width, self.bounds.height)
CGContextAddLineToPoint(context, 0, self.bounds.height)
CGContextSetFillColorWithColor(context,UIColor(red: 37/255, green: 88/255, blue: 120/255, alpha: 1.0).CGColor)
CGContextDrawPath(context, CGPathDrawingMode.Fill)
How can I animate the blue part from left to right, while keeping the gray "max" portion of the graph always visible?
drawRect is producing still picture. To get animation you're saying about I'd recommend the following:
Use CoreAnimation to produce animation
Use UIBezierPath to make a shape you need
Use CALayer's mask to animate within required shape
Here is example code for Playground:
import UIKit
import QuartzCore
import XCPlayground
let view = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 40))
XCPlaygroundPage.currentPage.liveView = view
let maskPath = UIBezierPath()
maskPath.moveToPoint(CGPoint(x: 10, y: 30))
maskPath.addLineToPoint(CGPoint(x: 10, y: 25))
maskPath.addLineToPoint(CGPoint(x: 100, y: 10))
maskPath.addLineToPoint(CGPoint(x: 100, y: 30))
maskPath.closePath()
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
maskLayer.fillColor = UIColor.whiteColor().CGColor
let rectToAnimateFrom = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 97, height: 40))
let rectToAnimateTo = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 0, height: 40))
let layerOne = CAShapeLayer()
layerOne.path = maskPath.CGPath
layerOne.fillColor = UIColor.grayColor().CGColor
let layerTwo = CAShapeLayer()
layerTwo.mask = maskLayer
layerTwo.fillColor = UIColor.greenColor().CGColor
view.layer.addSublayer(layerOne)
view.layer.addSublayer(layerTwo)
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = rectToAnimateFrom.CGPath
animation.toValue = rectToAnimateTo.CGPath
animation.duration = 1
animation.repeatCount = 1000
animation.autoreverses = true
layerTwo.addAnimation(animation, forKey: "Nice animation")
In your code, I only see you draw the graphic once, why not draw gray part first and then draw the blue part.
I don't think it is efficient enough to implement animation in drawRect function.
You can take a look at Facebook's Shimmer Example, it simulate the effect of iPhone unlock animation. It uses a mask layer. The idea could also work in your example.
Also, Facebook's pop framework could simplify your work.

Animate a UIView on a sine wave path

How would you make a UIView move along a sine wave path? Ideally, I'd be able to stop the animation when the UIView goes off the screen and release it. I see that UIView->animateWithDuration can only animate on one property at a time, but this seems like two exclusive things are happening simultaneously: it's moving to the right and it's moving up/down.
You can actually do this with a simple CAKeyframeAnimation.
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(x: 10, y: 100, width: 50, height: 50))
myView.backgroundColor = UIColor.cyan
view.addSubview(myView)
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.duration = 60 // 60 seconds
animation.isAdditive = true // Make the animation position values that we later generate relative values.
// From x = 0 to x = 299, generate the y values using sine.
animation.values = (0..<300).map({ (x: Int) -> NSValue in
let xPos = CGFloat(x)
let yPos = sin(xPos)
let point = CGPoint(x: xPos, y: yPos * 10) // The 10 is to give it some amplitude.
return NSValue(cgPoint: point)
})
myView.layer.add(animation, forKey: "basic")
}

Resources