CABasicAnimation creates empty default value copy of CALayer
Figured out the answer!
In init(layer:)
you have to copy the property values to your class manually. Here's how that looks in action:
override init(layer: AnyObject) {
if let layer = layer as? RadialGradientLayer {
gradientOrigin = layer.gradientOrigin
gradientRadius = layer.gradientRadius
colors = layer.colors
locations = layer.locations
}
super.init(layer: layer)
}
CALayer during animation is using default property value instead of what should be the current instance's property value
I figured out the solution for anyone in need. When a CALayer is animated, the CALayer isn't mutated, rather it is duplicated by using CALayer's init(layer:)
.
I was reading the documentation on this initializer (enter link description here) and found that inside the override you are suppose to copy layer
's custom properties to the new CALayer subclass.
public override init(layer: Any) {
super.init(layer: layer)
self.common()
guard let overlay = layer as? OverlayLayer else {
return
}
self.color = overlay.color
}
Here's an update Gist with the fix if you're curious
https://gist.github.com/Wowserman/ca23d9596f2979effd9cc8fa9e73c893
CABasicAnimation resets to initial value after animation completes
Here's the answer, it's a combination of my answer and Krishnan's.
cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;
The default value is kCAFillModeRemoved
. (Which is the reset behavior you're seeing.)
replacing CALayer and CABasicAnimation with SKScene and SKActions
You can easily achieve this kind of animation using moveToX
and the timingMode
parameter.
New Swift 3 translation below at the end of this answer.
To make an example I use the Xcode Sprite-Kit "Hello, World!" official project demo:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Hello, World!"
myLabel.fontSize = 15
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(myLabel)
mediaTimingFunctionEaseInEaseOutEmulate(myLabel)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKLabelNode) {
let actionMoveLeft = SKAction.moveToX(CGRectGetMidX(self.frame)-100, duration:1.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(CGRectGetMidX(self.frame)+100, duration:1.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
}
Output:
Update (This part start to emulate the static ball and the dynamic ball moving left and right but without metaball animations)
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let seq = SKAction.sequence([zoomIn,zoomOut])
ball.runAction(seq,withKey: "zoom")
}
}
i += 1
}
}
}
New update with metaball animation:
Finally I've realize this result, my goal is to make it very similar to the original :
is it possible to make some variations to times (for example zoomIn or zoomOut time values or actionMoveLeft, actionMoveRight time values), this is the code:
import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : NSTimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.moveToPoint(p1a)
pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLineToPoint(p2b)
pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLineToPoint(p1a)
pathJoinedCircles.closePath()
let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
shapeNode.strokeColor = SKColor.orangeColor()
shapeNode.fillColor = UIColor.clearColor()
addChild(shapeNode)
let wait = SKAction.waitForDuration(vanishingTime)
self.runAction(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRectMake(ball.frame.origin.x-self.radiusBall*3,ball.frame.origin.y,ball.frame.width+(self.radiusBall*6),ball.frame.height)
if CGRectContainsRect(enlargeFrame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.EaseInEaseOut
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let wait = SKAction.waitForDuration(0.8)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.runAction(seq,withKey: "zoom")
}
}
self._metaball(ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 4 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}
Swift 3:
(I've made a little change to maxDistance: 4 * self.radiusBall
with maxDistance: 5 * self.radiusBall
to become more similar to the original but you can change it as you wish)
import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMove(to view: SKView) {
let label = self.childNode(withName: "//helloLabel") as? SKLabelNode
label?.removeFromParent()
self.anchorPoint = CGPoint.zero
// Some parameters
let strokeColor = SKColor.orange
let dBHeight = self.frame.midY
let dBStartX = self.frame.midX-260 // extreme left
let dBStopX = self.frame.midX+260 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPoint(x:self.frame.midX, y:dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clear
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPoint(x:dBStartX+(distanceBtwBalls*CGFloat(i)), y:dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clear
if i == 0 || i == totalBalls-1 {
ball.isHidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(node: dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveTo(x: dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.easeInEaseOut
let actionMoveRight = SKAction.moveTo(x: dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.easeInEaseOut
node.run(SKAction.repeatForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : TimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(point: center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(point: center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(point: p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.move(to: p1a)
pathJoinedCircles.addCurve(to: p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLine(to: p2b)
pathJoinedCircles.addCurve(to: p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLine(to: p1a)
pathJoinedCircles.close()
let shapeNode = SKShapeNode(path: pathJoinedCircles.cgPath)
shapeNode.strokeColor = SKColor.orange
shapeNode.fillColor = UIColor.clear
addChild(shapeNode)
let wait = SKAction.wait(forDuration: vanishingTime)
self.run(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(_ currentTime: TimeInterval) {
var i = 0
self.enumerateChildNodes(withName: "ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRect(x:ball.frame.origin.x-self.radiusBall*3,y:ball.frame.origin.y,width:ball.frame.width+(self.radiusBall*6),height:ball.frame.height)
if enlargeFrame.contains(self.dBCircle.frame) {
if (ball.action(forKey: "zoom") == nil) {
let zoomIn = SKAction.scale(to: 1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.easeInEaseOut
let zoomOut = SKAction.scale(to: 1.0, duration: 0.25)
let wait = SKAction.wait(forDuration: 0.7)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.run(seq,withKey: "zoom")
}
}
self._metaball(circle2: ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 5 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}
How to animate a CALayer to have empty contents
I decided this was the cleanest approach, since there was no other answer forthcoming:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CABasicAnimation *backgroundOut = [CABasicAnimation animationWithKeyPath:@"contents"];
backgroundOut.toValue = (__bridge id)(blank.CGImage);
backgroundOut.fillMode = kCAFillModeForwards;
backgroundOut.removedOnCompletion = NO;
backgroundOut.duration = 3.0;
[_backgroundLayer addAnimation:backgroundOut forKey:@"contents"];
Related Topics
Querying in Firebase by Child of Child
Unit Testing Wknavigationdelegate Functions
Xcode 9 Fails to Build Swift 4 Project with Pod
Create Complicated Nscompoundpredicate in Swift 3
How to Detect the 2D Images Using Arkit and Realitykit
Play Segment of Avaudiopcmbuffer
How to Make Uicollectionview Reload Once It Receives Data from Firebase
Delegate Must Respond to Locationmanager:Didupdatelocations Swift Eroor
iOS Swift - Uitableviewcell Custom Subclass Not Displaying Content
How to Customize the Font and Appearance of a Uialertcontroller in the New Xcode W/ iOS8
New' Is Unavailable: You Cannot Directly Instantiate an Stpissuingcardpin
Connecting Hc-05 with iPhone Se iOS(V11.0)
Can You Enforce a Typealias in Swift
Limit Textfield to X Amount of Characters Using Swiftui
How to Use Tabs to Evenly Space Out Description Strings in Swift