Swift animation chaining using CGAffineTransforms - swift

I'm trying to create a background animation for my app. What I'm trying to achieve for part of it is:
1) An image (random colour / shape chosen from an array) appears randomly on the view. 2) It then grows in size rapidly (from not visible, to visible). 3) Then will slowly move in a given direction, while rotating, for 10ish seconds. 4) Then it will scale back down to a non-visible size and be removed from the view.
What I'm finding is that the shape will appear correctly in steps 1 and 2. Then, the shape will jump to a random position on the screen at the start of animation/step 3 (transition3 in the below code). While it moves, it also decreases in size. Then for step 4 it jumps back to it's original position in step 1 & 2, before shrinking off the screen as intended.
I cannot for the life of me work out what's going on here. I'm hoping I haven't missed something embarrassingly obvious.
Thanks in advance for any help!
class BackgroundAnimation {
func animation(animationView: UIView) {
let colourArray = [
UIColor(red:0.99, green:0.80, blue:0.05, alpha:1.0),
UIColor(red:0.06, green:0.22, blue:0.29, alpha:1.0),
UIColor(red:0.95, green:0.18, blue:0.30, alpha:1.0),
UIColor(red:0.35, green:0.77, blue:0.92, alpha:1.0),
UIColor(red:0.95, green:0.61, blue:0.19, alpha:1.0)
]
let imageArray = [
"Cross",
"Circle",
"Halfmoon",
"Square",
"Triangle"
]
//Animation constants
let initialDimensions = 10
let pathLength: CGFloat = 100
let pathDuration = 10
let scaleFactor: CGFloat = 5
let scaleDuration = 1
//Select the random image and random colour that is to be animated.
let image = UIImage(named: imageArray[Int.random(in: 0...imageArray.count - 1)])
let imageView = UIImageView(image: image)
imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = colourArray[Int.random(in: 0...colourArray.count - 1)]
imageView.contentMode = .scaleAspectFit
imageView.frame = CGRect(x: 0, y: 0, width: initialDimensions, height: initialDimensions)
animationView.insertSubview(imageView, at: 0)
//create a random start location and angle of direction
let startPointX = CGFloat.random(in: 0...animationView.frame.width)
let startPointY = CGFloat.random(in: 0...animationView.frame.height)
let pathAngle = CGFloat.random(in: 0...(CGFloat.pi * 2))
imageView.center = CGPoint(x: startPointX, y: startPointY)
//Calculate the endpoint from path angle and length
let translationX = pathLength * sin(pathAngle)
let tanslationY = -(pathLength * cos(pathAngle))
//Define the transitions for the aniamtion
var transition1 = CGAffineTransform.identity
transition1 = transition1.scaledBy(x: scaleFactor, y: scaleFactor)
var transition2 = CGAffineTransform.identity
transition2 = transition2.translatedBy(x: translationX, y: tanslationY)
transition2 = transition2.rotated(by: CGFloat.random(in: CGFloat.pi * 1/4 ... CGFloat.pi * 3/4))
var transition3 = CGAffineTransform.identity
transition3 = transition3.scaledBy(x: 0.001, y: 0.001)
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition1
}, completion: {finished in
UIView.animate(withDuration: TimeInterval(pathDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition2
}, completion: { finished in
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition3
}, completion: { finished in
imageView.removeFromSuperview()
})
})
})
}
}

change your position with imageView.center OR imageView.frame.origin
imageView.frame.origin = CGPoint(x: translationX, y: tanslationY)
but you should calculate the destination, then set frame.origin to the destination

In ref to Ehsan's helpful response:
This has fixed the movement issue, but the imageView is getting smaller when I try to apply the rotation in the second animation block. Updated code below:
class BackgroundAnimation {
func animation(animationView: UIView) {
let colourArray = [
UIColor(red:0.99, green:0.80, blue:0.05, alpha:1.0),
UIColor(red:0.06, green:0.22, blue:0.29, alpha:1.0),
UIColor(red:0.95, green:0.18, blue:0.30, alpha:1.0),
UIColor(red:0.35, green:0.77, blue:0.92, alpha:1.0),
UIColor(red:0.95, green:0.61, blue:0.19, alpha:1.0)
]
let imageArray = [
"Cross",
"Circle",
"Halfmoon",
"Square",
"Triangle"
]
//Animation constants
let initialDimensions = 10
let pathLength: CGFloat = 100
let pathDuration = 10
let scaleFactor: CGFloat = 3
let scaleDuration = 1
//Select the random image and random colour that is to be animated.
let image = UIImage(named: imageArray[Int.random(in: 0...imageArray.count - 1)])
let imageView = UIImageView(image: image)
imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = colourArray[Int.random(in: 0...colourArray.count - 1)]
imageView.contentMode = .scaleAspectFit
imageView.frame = CGRect(x: 0, y: 0, width: initialDimensions, height: initialDimensions)
animationView.insertSubview(imageView, at: 0)
//create a random start location and angle of direction
let startPointX = CGFloat.random(in: 0...animationView.frame.width * 0.9)
let startPointY = CGFloat.random(in: 0...animationView.frame.height * 0.9)
let pathAngle = CGFloat.random(in: 0...(CGFloat.pi * 2))
imageView.center = CGPoint(x: startPointX, y: startPointY)
//Calculate the endpoint from path angle and length
let translationX = pathLength * sin(pathAngle)
let translationY = (pathLength * cos(pathAngle))
let endPointX = startPointX + translationX
let endPointY = startPointY + translationY
//Define the transitions for the aniamtion
var transition1 = CGAffineTransform.identity
transition1 = transition1.scaledBy(x: scaleFactor, y: scaleFactor)
var transition2 = CGAffineTransform.identity
transition2 = transition2.rotated(by: CGFloat.random(in: CGFloat.pi * 1/4 ... CGFloat.pi * 3/4))
var transition3 = CGAffineTransform.identity
transition3 = transition3.scaledBy(x: 0.001, y: 0.001)
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0, options: .beginFromCurrentState, animations: {
imageView.transform = transition1
}, completion: {finished in
UIView.animate(withDuration: TimeInterval(pathDuration), delay: 0.5, options: [.beginFromCurrentState, .curveLinear], animations: {
imageView.frame.origin = CGPoint(x: endPointX, y: endPointY)
imageView.transform = transition2
}, completion: { finished in
UIView.animate(withDuration: TimeInterval(scaleDuration), delay: 0.5, options: .beginFromCurrentState, animations: {
imageView.transform = transition3
}, completion: { finished in
imageView.removeFromSuperview()
})
})
})
}
}

Related

Align a node with its neighbor in SceneKit

Using Swift 5.5, iOS 14
Trying to create a simple 3D bar chart and I immediately find myself in trouble.
I wrote this code...
var virgin = true
for i in stride(from: 0, to: 6, by: 0.5) {
let rnd = CGFloat.random(in: 1.0...4.0)
let targetGeometry = SCNBox(width: 0.5,
height: rnd,
length: 0.5,
chamferRadius: 0.2)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let box = SCNNode(geometry: targetGeometry)
box.simdPosition = SIMD3(x: 0, y: 0, z: 0)
coreNode.addChildNode(box)
}
This works well, but all the bars a centred around their centre. But how can I ask SceneKit to change the alignment?
Almost got this working with this code...
box.simdPosition = SIMD3(x: Float(i) - 3,
y: Float(rnd * 0.5),
z: 0)
But the result isn't right... I want/need the bar to grow from the base.
https://youtu.be/KJgvdBFBfyc
How can I make this grow from the base?
--Updated with working solution--
Tried Andy Jazz suggestion replacing simdposition with the following formulae.
box.simdPosition = SIMD3(x:box.simdPosition.x, y:0, z:0)
box.simdPivot.columns.3.y = box.boundingBox.max.y - Float(rnd * 0.5)
Worked well, to which I added some animation! Thanks Andy.
changer = changeling.sink(receiveValue: { [self] _ in
var rnds:[CGFloat] = []
for i in 0..<12 {
let rnd = CGFloat.random(in: 1.0...2.0)
let targetGeometry = SCNBox(width: 0.45, height: rnd, length: 0.45, chamferRadius: 0.01)
let newNode = SCNNode(geometry: targetGeometry)
sourceNodes[i].simdPivot.columns.3.y = newNode.boundingBox.min.y
sourceNodes[i].simdPosition = SIMD3(x: sourceNodes[i].simdPosition.x, y: 0, z: 0)
rnds.append(rnd)
}
for k in 0..<12 {
if virgin {
coreNode.addChildNode(sourceNodes[k])
}
let targetGeometry = SCNBox(width: 0.45, height: rnds[k], length: 0.45, chamferRadius: 0.01)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let morpher = SCNMorpher()
morpher.targets = [targetGeometry]
sourceNodes[k].morpher = morpher
let animation = CABasicAnimation(keyPath: "morpher.weights[0]")
animation.toValue = 1.0
animation.repeatCount = 0.0
animation.duration = 1.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
sourceNodes[k].addAnimation(animation, forKey: nil)
}
virgin = false
})
You have to position a pivot point of each bar to its base:
boxNode.simdPivot.columns.3.y = someFloatNumber
To reposition a pivot to bar's base use bounding box property:
boxNode.simdPivot.columns.3.y += (boxNode.boundingBox.min.y as? simd_float1)!
After pivot's offset, reposition boxNode towards negative direction of Y-axis.
boxNode.position.y = 0

EmitterCells not showing in CAEmitterLayer

I can't seem to get the CAEmitterLayer display CAEmitterCells. I add the ConfettiEmitterView in Storyboard and the view is being displayed with a red background as intended. But the cells are not showing. No Errors or warinings either.
The code:
import UIKit
class ConfettiEmitterView: UIView {
override class var layerClass:AnyClass {
return CAEmitterLayer.self
}
func makeEmmiterCell(color:UIColor, velocity:CGFloat, scale:CGFloat)-> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 10
cell.lifetime = 20.0
cell.lifetimeRange = 0
cell.velocity = velocity
cell.velocityRange = velocity / 4
cell.emissionLongitude = .pi
cell.emissionRange = .pi / 8
cell.scale = scale
cell.scaleRange = scale / 3
cell.contents = roundImage(with: .yellow)
return cell
}
override func layoutSubviews() {
let emitter = self.layer as! CAEmitterLayer
emitter.frame = self.bounds
emitter.masksToBounds = true
emitter.emitterShape = .line
emitter.emitterPosition = CGPoint(x: bounds.midX, y: 0)
emitter.emitterSize = CGSize(width: bounds.size.width, height: 1)
emitter.backgroundColor = UIColor.red.cgColor // Setting the background to red
let near = makeEmmiterCell(color: UIColor(white: 1, alpha: 1), velocity: 100, scale: 0.3)
let middle = makeEmmiterCell(color: UIColor(white: 1, alpha: 0.66), velocity: 80, scale: 0.2)
let far = makeEmmiterCell(color: UIColor(white: 1, alpha: 0.33), velocity: 60, scale: 0.1)
emitter.emitterCells = [near, middle, far]
}
func roundImage(with color: UIColor) -> UIImage {
let rect = CGRect(origin: .zero, size: CGSize(width: 12.0, height: 12.0))
return UIGraphicsImageRenderer(size: rect.size).image { context in
context.cgContext.setFillColor(color.cgColor)
context.cgContext.addPath(UIBezierPath(ovalIn: rect).cgPath)
context.cgContext.fillPath()
}
}
}
Adding a view of type ConfettiEmitterView to the UIViewController in Storyboard should be enough to recreate the issue.
Change
cell.contents = roundImage(with: .yellow)
To
cell.contents = roundImage(with: .yellow).cgImage

How can i put shake Animation on UIImageView

I am trying to add permanent Shake Animation on UIImageView.
How can i control its Animation timings as well.
var coffeeImageView = UIImageView(image: UIImage(named: "coffee.png"))
shakeImg.frame = CGRectMake(100, self.view.frame.size.height - 100, 50, 50)
self.view.addSubview(coffeeImageView)
let coffeeShakeAnimation = CABasicAnimation(keyPath: "position")
coffeeShakeAnimation.duration = 0.07
coffeeShakeAnimation.repeatCount = 20
coffeeShakeAnimation.autoreverses = true
coffeeShakeAnimation.fromValue = NSValue(cgPoint: CGPointMake(shakeImg.center.x - 10, shakeImg.center.y))
coffeeShakeAnimation.toValue = NSValue(cgPoint: CGPointMake(shakeImg.center.x + 10, shakeImg.center.y))
shakeImg.layer.add(coffeeShakeAnimation, forKey: "position")
You need
var coffeeImageView = UIImageView(image: UIImage(named: "coffee.png"))
coffeeImageView.frame = CGRect(x: 100, y: self.view.frame.size.height - 100, width: 50, height: 50)
self.view.addSubview(coffeeImageView)
let coffeeShakeAnimation = CABasicAnimation(keyPath: "position")
coffeeShakeAnimation.duration = 0.07
coffeeShakeAnimation.repeatCount = 20
coffeeShakeAnimation.autoreverses = true
coffeeShakeAnimation.fromValue = NSValue(cgPoint: CGPoint(x: coffeeImageView.center.x - 10, y: coffeeImageView.center.y))
coffeeShakeAnimation.toValue = NSValue(cgPoint: CGPoint(x: coffeeImageView.center.x + 10, y: coffeeImageView.center.y))
coffeeImageView.layer.add(coffeeShakeAnimation, forKey: "position")
extension UIView {
func shake(_ dur:Double) {
let anim = CABasicAnimation(keyPath: "position")
anim.duration = dur
anim.repeatCount = 20
anim.autoreverses = true
anim.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
anim.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
self.layer.add(anim, forKey: "position")
}
}
I would prefer using UIView Extension to achieve this animation with reusability.
Swift 4
extension UIView {
func shake(_ duration: Double? = 0.4) {
self.transform = CGAffineTransform(translationX: 20, y: 0)
UIView.animate(withDuration: duration ?? 0.4, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
self.transform = CGAffineTransform.identity
}, completion: nil)
}
}
Usage
coffeeImageView.shake()
coffeeImageView.shake(0.2)
Try this!
class func shakeAnimation(_ view: UIView) {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.06
animation.repeatCount = 2
animation.autoreverses = true
animation.isRemovedOnCompletion = true
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animation.fromValue = NSValue(cgPoint: CGPoint(x: view.center.x - 7, y: view.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: view.center.x + 7, y: view.center.y))
view.layer.add(animation, forKey: nil)
}

Getting SpriteKit texture from SKView returning nil after 104 calls

I am having some odd behavior in SpriteKit when creating a texture. The function below shows you what I am doing. In short, I'm in SceneKit and making a SCNNode out of an Array of colors (think pixel/voxels). It works like a charm. However, after exactly 104 calls the texture returned is nil. Afterwards, it is hit or miss whether the texture will be nil or not. I am also providing the exact color information. Thoughts?
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}
This is where autoreleasepool comes in handy, it allows you to release the memory when the autoreleasepool is finished so that you do not run out of space before using it again.
Of course this is not going to solve your main problem, where you are creating too many textures and running out of memory space, but it will allow you to at least make some more because it will release the temporary memory that view.texture(from:node) is holding on to.
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
autoreleasepool{
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
}
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}

Im not able to give a new corner radius to my view

I'm creating some circles in a view, and I want to resize them to their previous position if they are somewhere near the next circle when I resize them with gestures on sender.state == .ended, they resize but it doesn't change the corner radius I want o give them on resize.
let circle = UIView(frame: CGRect(x: 0.0, y: 0.0, width: a, height: a))
circle.center = self.view.center
circle.layer.cornerRadius = CGFloat(cornerRad)
circle.backgroundColor = UIColor.black.withAlphaComponent(0.0)
circle.layer.borderWidth = CGFloat(b)
circle.layer.borderColor = UIColor.red.cgColor
circle.clipsToBounds = true
let pinchCircle = UIPinchGestureRecognizer(target: self, action: #selector(changeSize))
//let longPressCircle = UILongPressGestureRecognizer(target: self, action: #selector(erraseCircle))
circle.addGestureRecognizer(pinchCircle)
//circle.addGestureRecognizer(longPressCircle)
self.view.addSubview(circle)
arrayShapes.append(circle)
if let view = sender.view {
view.transform = CGAffineTransform(scaleX: sender.scale, y: sender.scale)
let maxScale: CGFloat = 300 //Anything
let minScale: CGFloat = 0.5 //Anything
let scale = view.frame.size.width
if scale > maxScale {
view.transform = CGAffineTransform(scaleX: maxScale / scale, y: maxScale / scale)
arrFin[pos] = Double(view.frame.size.width)
}
else if scale < minScale {
view.transform = CGAffineTransform(scaleX: minScale / scale, y: minScale / scale)
arrFin[pos] = Double(view.frame.size.width)
} else {
arrFin[pos] = Double(view.frame.size.width)
}
for (i, _) in arrayShapes.enumerated() {
if arrayShapes[i].frame.width < view.frame.size.width{
self.view.sendSubview(toBack: view)
}
}
if sender.state == .ended {
for (z, _) in arrFin.enumerated() where z != pos{
let sum = arrFin[pos] - arrFin[z]
let res = arrFin[z] - arrFin[pos]
if arrFin[pos] == arrFin[z] || sum <= 20 || res >= 20{
arrFin[pos] = arrPos[pos]
sender.view?.layer.masksToBounds = true
sender.view?.layer.cornerRadius = CGFloat(arrPos[pos]) / 2
sender.view?.frame.size.width = CGFloat(arrPos[pos])
sender.view?.frame.size.height = CGFloat(arrPos[pos])
sender.view?.center = self.view.center
print(arrPos[pos])
break
}
}
}
The solution was using sender.view?.transform = CGAffineTransform.identity on the for loop instead of trying to force the size.
The problem was, that i was trying to resize to previous values but my scaling was messing those numbers up.