Search code examples
iosswiftanimationuiimageviewcore-animation

How to animate drawing in Swift, but also change a UIImageView's scale?


I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:

func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations)   // do some math to figure the best scale

UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale)      // some animation prohibits changes!
// ...  drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}

For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.

Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?


Solution

  • Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values

    Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.

    You'll need a special layer:

    class MyLayer : CALayer {
        @objc var spirality : CGFloat = 0
        override class func needsDisplay(forKey key: String) -> Bool {
            if key == #keyPath(spirality) {
                return true
            }
            return super.needsDisplay(forKey:key)
        }
        override func draw(in con: CGContext) {
            print(self.spirality) // in real life, this is our signal to draw!
        }
    }
    

    The layer must actually be in the interface, though it can be impossible for the user to see:

    let lay = MyLayer()
    lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
    self.view.layer.addSublayer(lay)
    

    Subsequently, we can initialize the spirality of the layer:

    lay.spirality = 2.0
    lay.setNeedsDisplay() // prints: 2.0
    

    Now when we want to "animate" the spirality, this is what we do:

    let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
    ba.fromValue = lay.spirality
    ba.toValue = 2.75
    ba.duration = 1
    lay.add(ba, forKey:nil)
    CATransaction.setDisableActions(true)
    lay.spirality = 2.75
    

    The console shows the arrival of a succession of intermediate values over the course of 1 second!

    2.03143266495317
    2.04482554644346
    2.05783333256841
    2.0708108600229
    2.08361491002142
    2.0966724678874
    2.10976020619273
    2.12260236591101
    2.13551922515035
    2.14842618256807
    2.16123360767961
    2.17421661689878
    2.18713565543294
    2.200748950243
    2.21360073238611
    2.2268518730998
    2.23987507075071
    2.25273013859987
    2.26560932397842
    2.27846492826939
    2.29135236144066
    2.30436328798532
    2.31764804571867
    2.33049770444632
    2.34330793470144
    2.35606706887484
    2.36881992220879
    2.38163591921329
    2.39440815150738
    2.40716737508774
    2.42003352940083
    2.43287514150143
    2.44590276479721
    2.45875595510006
    2.47169743478298
    2.48451870679855
    2.49806520342827
    2.51120449602604
    2.52407149970531
    2.53691896796227
    2.54965999722481
    2.56257836520672
    2.57552136480808
    2.58910304307938
    2.60209316015244
    2.6151298135519
    2.62802086770535
    2.64094598591328
    2.6540260463953
    2.6669240295887
    2.6798157542944
    2.69264766573906
    2.70616912841797
    2.71896715462208
    2.73285858333111
    2.74564798176289
    2.75
    2.75
    2.75
    

    Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.