CALayer Mask Animation not disappearing - ios

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.

Related

How to flip a card while scaling it in Swift

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")

Cut transparent hole in the shape of UIImage into UIView

I have an UIView in which I want to create a hole in the shape of the content of a UIImageView, but I can't get it to work. I can, however, mask an UIView to the shape of the UIImage.
My code:
let doorsMaskBounds = CGRect(x: 0, y: 0, width: 111, height: 111)
// Background view
let doorsBgView = UIView(frame: doorsMaskBounds)
doorsBgView.center = navigationController.view.center
doorsBgView.backgroundColor = UIColor.yellow
vc.view.addSubview(doorsBgView)
vc.view.bringSubview(toFront: doorsBgView)
// Logo Mask
doorsBgView.layer.mask = CALayer()
doorsBgView.layer.mask?.contents = UIImage(named: "doors")!.cgImage
doorsBgView.layer.mask?.bounds = doorsMaskBounds
doorsBgView.layer.mask?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
doorsBgView.layer.mask?.position = CGPoint(x: doorsBgView.frame.width / 2, y: doorsBgView.frame.height / 2)
which results into:
I know how to "cut" a hole into a UIView in form of a shape I draw myself, as explained here, I just can't seem to figure out how the get the shape of the UIImage and apply it as the cut.

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.

Circular transition (mask) in iOS and Objective C

We were wondering how we can easily build a circular mask, that blends out the background and transitions into a new view, with new buttons? See example here (watch the red planet gets triggered):
//Swift 4
This is a simple static method for circular bubble transition from a point screen.
//as shown on this link
https://github.com/andreamazz/BubbleTransition
import UIKit
class AnimationUtility: UIViewController {
static func animateBubbleTrnsitionView( selfView: UIView, point : CGPoint) {
//let button = CGRect.init(x: 30, y: selfView.frame.size.height - 15, width: 45, height: 45)
let button = CGRect.init(x: point.x, y: point.y, width: 0, height: 0)
let circleMaskPathInitial = UIBezierPath(ovalIn: CGRect.init(x: point.x, y: point.y, width: 1, height: 1))
let extremePoint = CGPoint(x: point.x, y: 15 - selfView.frame.size.height - 200)
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(ovalIn: (button.insetBy(dx: -radius, dy: -radius)))
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.cgPath
selfView.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
maskLayerAnimation.duration = 0.9
maskLayer.add(maskLayerAnimation, forKey: "path")
}
}
Use this code in following way
Perform segue or push without animation.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
switch segue.identifier! {
case "navToHomeWithoutanimation":
self.navigationController?.view.backgroundColor = APP_ORANGE_COLOR //which ever color you want
let vc = segue.destination as! MapViewController
AnimationUtility.animateBubbleTrnsitionView(selfView: vc.view, point: self.view.center)
break
}
}
//The animation will start to animate from given point.
Swift code :
I writted all code appdelegate file. Used a screenshot of timeline rather than a full-blown UITableView.
First, let’s add the screenshot on the window:
let imageView = UIImageView(frame: self.window!.frame)
imageView.image = UIImage(named: "twitterscreen")
self.window!.addSubview(imageView)
self.mask = CALayer()
self.mask!.contents = UIImage(named: "twitter logo mask").CGImage
self.mask!.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
self.mask!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.mask!.position = CGPoint(x: imageView.frame.size.width/2, y: imageView.frame.size.height/2)
imageView.layer.mask = mask
animating the mask. You’ll observe that the bird reduces in size for a bit, and then increases in size, so to do that using just one animation, let’s use CAKeyframeAnimation.
let keyFrameAnimation = CAKeyframeAnimation(keyPath: "bounds")
keyFrameAnimation.duration = 1
keyFrameAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut), CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)]
let initalBounds = NSValue(CGRect: mask!.bounds)
let secondBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width: 90, height: 90))
let finalBounds = NSValue(CGRect: CGRect(x: 0, y: 0, width: 1500, height: 1500))
keyFrameAnimation.values = [initalBounds, secondBounds, finalBounds]
keyFrameAnimation.keyTimes = [0, 0.3, 1]
self.mask!.addAnimation(keyFrameAnimation, forKey: "bounds")
If you see result please visit the github repo. https://github.com/rounak/TwitterBirdAnimation/

Resources