jwalk
jwalk

Reputation: 29

Why does CGAffineTransform(scaleX:y:) cause my view to rotate as if I'd called CGAffineTransform(rotationAngle:)?

I took Paul Hudson's (Hacking with Swift) animation tutorial (His Project 15) and tried to extend it to see the effect of multiple animations layered on one another.

There are four distinct animations: scale, translate, rotate and color. Instead of a single button that cycles through all of them, as in his tutorial, I used four buttons to allow selection of each animation individually. I also modified his "undo" animation by reversing the original animation rather than using CGAffineTransform.identity, since the identity transform would undo all the animations to that point.

My problem is that when I click my c button, it does the appropriate scaling the first click but, rather than scale the penguin back to its original size on the second click, it rotates the view. Subsequent clicks of the scale button continue to rotate the view as if I were clicking the rotate button.

There are other anomalies, such as the first click of the move button moves the penguin appropriately but following that with a click of the rotate button, both rotates the penguin and moves it back to the original position. I'm not sure why it moves back but I accept that that might be my own ignorance of the animation system.

I've added print statements and put in breakpoints to debug the scale problem. Everything in the code seems to be working exactly as coded but the animations defy logic! Any help would be appreciated.

The complete program is relatively simple:

import UIKit

class ViewController: UIViewController {
    var imageView: UIImageView!
    var scaled = false
    var moved = false
    var rotated = false
    var colored = false
    
    @IBOutlet var scaleButton: UIButton!
    @IBOutlet var moveButton: UIButton!
    @IBOutlet var rotateButton: UIButton!
    @IBOutlet var colorButton: UIButton!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        imageView = UIImageView(image: UIImage(named: "penguin"))
        imageView.center = CGPoint(x: 200, y: 332)
        view.addSubview(imageView)
    }

    @IBAction func tapped(_ sender: UIButton) {
        sender.isHidden = true
        var theAnimation: ()->Void
        
        switch sender {
        case scaleButton:
            theAnimation = scaleIt
        case moveButton:
            theAnimation = moveIt
        case rotateButton:
                theAnimation = rotateIt
        case colorButton:
                theAnimation = colorIt
        default:
            theAnimation = { self.imageView.transform = .identity }
        }
        print("scaled = \(scaled), moved = \(moved), rotated = \(rotated), colored = \(colored)")
    
        UIView.animate(withDuration: 1, delay: 0, options: [], animations: theAnimation) { finished in
            sender.isHidden = false
        }
    }
    
    func scaleIt() {
        print("scaleIt()")
        let newScale: CGFloat = self.scaled ? -1.5 : 1.5
        self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
        self.scaled.toggle()
    }
    
    func moveIt() {
        print("moveIt()")
        let newX: CGFloat = self.moved ? 0 : -50
        let newY: CGFloat = self.moved ? 0 : -150
        self.imageView.transform = CGAffineTransform(translationX: newX, y: newY)
        self.moved.toggle()
    }
    
    func rotateIt() {
        print("rotateIt()")
        let newAngle = self.rotated ? 0.0 : CGFloat.pi
        self.imageView.transform = CGAffineTransform(rotationAngle: newAngle)
        self.rotated.toggle()
    }
    
    func colorIt() {
        print("colorIt()")
        let newAlpha: CGFloat = self.colored ? 1 : 0.1
        let newColor = self.colored ? UIColor.clear : UIColor.green
        self.imageView.alpha = newAlpha
        self.imageView.backgroundColor = newColor
        self.colored.toggle()
    }
    
}

By any combination of clicking the buttons, I can only arrive at 10 configurations of the Penguin (five with CGAffineTransforms and the same five modified by the color button).

I'll use these images to answer some of your questions. I've tried to label these images "Configuration1A, Configuration2A, ... Configuration1B", where the B configurations are identical to the A configurations but with the color button's effect. Perhaps the labels will show up in my post (I'm very new to posting on StackOverflow).

Here are the five configurations with the first one repeated using the color button:

Configuration1A

Configuration2A

Configuration3A

Configuration4A

Configuration5A

Configuration1B (same as 1A but with color button)

I first tried repeated button pressings. For each series of pressing a given button, I first returned the penguin to its original position, size, angle and color. For these repeated button pressings, the behavior I observed was as follows:

Button Observed behavior

  1. scale Penguin scales from Configuration1A to Configuration2A (as expected).
  2. scale Penguin rotates by pi from Configuration2A to Configuration4A (WHAT???).
  3. scale Penguin rotates by pi from Configuration4A to Configuration2A (WHAT???).

scale ... steps 2 and 3, above, repeat indefinitely (WHAT???).

  1. move Penguin moves (-50, -150), Configuration1A to Configuration3A (as expected).

  2. move Penguin moves (50, 150) Configuration3A to Configuration1A (as expected).

  3. move ... behavior above repeats indefinitely (as expected).

  4. rotate Penguin rotates by pi, Configuration1A to Configuration5A (as expected).

  5. rotate Penguin rotates by pi, Configuration5A to Configuration1A (as expected).

  6. rotate ... behavior above repeats indefinitely (as expected).

  7. color Penguin's color changes, Configuration1A to Configuration1B (as expected).

  8. color Penguin's color changes, Configuration1B to Configuration1A (as expected).

  9. color ... behavior above repeats indefinitely (as expected).

Upvotes: 2

Views: 2202

Answers (1)

Duncan C
Duncan C

Reputation: 131491

The opposite of scale 1.5 (50% bigger) is not -1.5, it's 0.5. (50% of it's original size)

Actually, wouldn't you want it to alternate between a scale of 1.5 (50% bigger) and 1.0 (normal size?)

Assuming you do want to alternate between 50% bigger and 50% smaller, change your scaleIt function to:

func scaleIt() {
    print("scaleIt()")
    let newScale: CGFloat = self.scaled ? -1.5 : 0.5
    self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
    self.scaled.toggle()
}

When you set the X or Y scale to a negative number, it inverts the coordinates in that dimension. Inverting in both dimensions will appear like a rotation.

As somebody else mentioned, the way you've written your functions, you won't be able to combine the different transforms. You might want to rewrite your code to apply the changes to the view's existing transform:

func scaleIt() {
    print("scaleIt()")
    // Edited to correct the math if you are applying a scale to an existing transform
    let scaleAdjustment: CGFloat = self.scaled ? -1.5 : 0.66666
    self.imageView.transform = self.imageView.transform.scaledBy(x: scaleAdjustment, y: scaleAdjustment)
    self.scaled.toggle()
}

Edit:

Note that changes to transforms are not "commutative", which is a fancy way of saying that the order in which you apply them matters. Applying a shift, then a rotate, will give different results than applying a rotate, then a shift, for example.

Edit #2:

Another thing:

The way you've written your functions, they will set the view to its current state, and then invert that current state for next time (you toggle scaled after deciding what to do.)

That means that for function like moveIt() nothing will happen the first time you tap the button. Rewrite that function like this:

func moveIt() {
    print("I like to moveIt moveIt!")
    self.moved.toggle() //Toggle the Bool first
    let newX: CGFloat = self.moved ? 0 : -50
    let newY: CGFloat = self.moved ? 0 : -150
    self.imageView.transform = self.imageView.transform.translatedBy(x: newX, y: newY)
}

Upvotes: 1

Related Questions