user3069232 user3069232 - 1 month ago 12
iOS Question

Creating raining code Matrix effect

Created a sub class of a CATextLayer within which I attached a fadeIn animation, which I than add to a CATextLayer to which I have attached a dropThru animation. The goal to try and create matrix movie raining code effect. Works reasonably well but for the fact that it slowly but surely drives itself into the ground, I suspect cause I keep adding more and more layers. How can I detect when an layer had left the screen so I may delete it.

Here is the code...

class CATextSubLayer: CATextLayer, CAAnimationDelegate {

private var starter:Float!
private var ender:Float!

required override init(layer: Any) {
super.init(layer: layer)
//UIFont.availableFonts()
self.string = randomString(length: 1)
self.backgroundColor = UIColor.black.cgColor
self.foregroundColor = UIColor.white.cgColor
self.alignmentMode = kCAAlignmentCenter
self.font = CTFontCreateWithName("AvenirNextCondensed-BoldItalic" as CFString?, fontSize, nil)
self.fontSize = 16
self.opacity = 0.0
makeFade()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")

}

func randomString(length: Int) -> String {

let letters : NSString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let len = UInt32(letters.length)

var randomString = ""
for _ in 0 ..< length {
let rand = arc4random_uniform(len)
var nextChar = letters.character(at: Int(rand))
randomString += NSString(characters: &nextChar, length: 1) as String
}

return randomString
}

func makeFade() {
let rands = Double(arc4random_uniform(UInt32(4)))
let fadeInAndOut = CABasicAnimation(keyPath: "opacity")
fadeInAndOut.duration = 16.0;
fadeInAndOut.repeatCount = 1
fadeInAndOut.fromValue = 0.0
fadeInAndOut.toValue = 1
fadeInAndOut.isRemovedOnCompletion = true
fadeInAndOut.fillMode = kCAFillModeForwards;
fadeInAndOut.delegate = self
fadeInAndOut.beginTime = CACurrentMediaTime() + rands
self.add(fadeInAndOut, forKey: "opacity")

}

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.removeAllAnimations()
}
}


With the outer loop/View Controller ..

class ViewController: UIViewController, CAAnimationDelegate {

var beeb: CATextSubLayer!
var meeb: CATextLayer!
var lines = [Int]()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
// Do any additional setup after loading the view, typically from a nib.
meeb = CATextLayer()
for bing in stride(from:0, to: Int(view.bounds.width), by: 16) {
lines.append(bing)
}
for _ in 0 ..< 9 {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(makeBeeb), userInfo: nil, repeats: true)
}
}

func makeBeeb() {
let rands = Double(arc4random_uniform(UInt32(4)))
let beeb = CATextSubLayer(layer: meeb)
let randx = Int(arc4random_uniform(UInt32(lines.count)))
let monkey = lines[randx]
beeb.frame = CGRect(x: monkey, y: 0, width: 16, height: 16)
let dropThru = CABasicAnimation(keyPath: "position.y")
dropThru.duration = 12.0;
dropThru.repeatCount = 1
dropThru.fromValue = 1
dropThru.toValue = view.bounds.maxY
dropThru.isRemovedOnCompletion = true
dropThru.fillMode = kCAFillModeForwards;
dropThru.beginTime = CACurrentMediaTime() + rands
dropThru.delegate = self
beeb.add(dropThru, forKey: "position.y")
self.view.layer.addSublayer(beeb)
}

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.view.layer.removeAllAnimations()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}


}

Answer

As far as I understand your code you can remove the layer, when it's position animation ends. In this moment it should have left the bounds of the parent view.

Btw. Removing and adding layers costs performance. Instead removing you should reuse the layer for the next animation.