Drehe Rechteck und kontrolliere Animation mit Slider - wurzelsand/uikit-memos GitHub Wiki

Drehe Rechteck und kontrolliere Animation mit Slider

Aufgabe

Das Rechteck soll bei 15º Neigung starten und fast eine komplette Drehung im Uhrzeigersinn machen (345º). Beim Berühren soll die Animation starten. Dabei bewegt sich ein Slider mit. Mit diesem Slider lässt sich durch die gesamte Animation scrollen.

Themen

  • UIViewPropertyAnimator um die Animation zu steuern.
  • NSLayoutConstraints um das Rechteck zu zentrieren.
  • CADisplayLink um den Slider bei jedem Bildaufbau zu aktualisieren und so mit der Animation zu synchronisieren.

Ausführung

import UIKit

class ViewController: UIViewController {
    let slider = UISlider()
    var displayLink: CADisplayLink?
    let animator = UIViewPropertyAnimator(duration: 2, curve: .easeInOut)

    override func viewDidLoad() {
        super.viewDidLoad()

        let redBox = UIView()
        view.addSubview(redBox)
        
        redBox.backgroundColor = .red
        redBox.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(
            [redBox.widthAnchor.constraint(equalToConstant: 150),
             redBox.heightAnchor.constraint(equalToConstant: 100),
             redBox.centerXAnchor.constraint(equalTo: view.centerXAnchor),
             redBox.centerYAnchor.constraint(equalTo: view.centerYAnchor)]
        )
        redBox.transform = CGAffineTransform(rotationAngle: 1 / 12 * .pi )
        
        view.addSubview(slider)
        
        slider.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(
            [slider.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -32),
             slider.widthAnchor.constraint(equalTo: view.widthAnchor)]
        )
        slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
        
        animator.addAnimations {
            redBox.rotate(by: (2 - 1 / 12) * .pi)
        }
        animator.pausesOnCompletion = true
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        displayLink = CADisplayLink(target: self, selector: #selector(update))
        displayLink?.add(to: .main, forMode: .common)
        animator.startAnimation()
    }
    
    @objc func sliderChanged(_ sender: UISlider) {
        animator.fractionComplete = CGFloat(sender.value)
    }
    
    @objc func update() {
        slider.value = Float(animator.fractionComplete)
        if animator.fractionComplete == 1 {
            animator.pauseAnimation()
            displayLink?.invalidate()
            displayLink = nil
        }
    }
}

public extension UIView {
    /**
     - Parameter by: Positive values for clockwise rotation, negative values for contra clockwise rotation.
                     Multiple full rotations are possible bei choosing angles > 2π.
     */
    func rotate(by angle: CGFloat) {
        let fraction: CGFloat = 3 / 4 * .pi
        let times = Int((abs(angle) / fraction))
        let remainder = angle.truncatingRemainder(dividingBy: fraction)
        for _ in 0..<times {
            transform = transform.rotated(by: angle >= 0 ? fraction : -fraction)
        }
        transform = transform.rotated(by: remainder)
    }
}

Anmerkungen

  • Statt CGAffineTransformation(rotationAngle:) hätte ich auch CATransform3DRotate wählen können.
  • CGAffineTransformation ergibt bei positiven Winkeln bis einschließlich 𝞹 eine Rotation im Uhrzeigersinn, bei negativen Winkeln die größer als -𝞹 sind eine Rotation entgegen dem Uhrzeigersinn. Winkel die sich nur um einen geringen Betrag von 𝞹 oder -𝞹 unterscheiden, können in Kombination mit anderen Transformationen Rundungsfehler ergeben. Daher habe ich den Gesamtwinkel durch ¾𝞹 geteilt und nicht durch 𝞹.
  • Ich beobachte, dass sich der Slider beim Abspielen der Animation gleichmäßig schnell bewegt, während sich gleichzeitig die Rotation des Rechtecks durch die easeInOut-Curve beschleunigt und wieder verlangsamt. Steuer ich hingegen die Rotation durch eine gleichmäßige Bewegung des Sliders, ist auch die Rotation völlig gleichförmig. Außerdem springt der Slider vor oder zurück, nachdem ich den Slider bewegt habe und anschließend die Animation fortsetze. Es macht also einen Unterschied, ob ich fractionComplete lese oder setze. Beim Lesen ergibt fractionComplete wieviel Prozent der Zeit vergangen ist, während ich beim Setzen festlege, wieviel Prozent des Zielwertes (Position, Rotationswinkel, Alpha-Wert etc.) erreicht ist.
⚠️ **GitHub.com Fallback** ⚠️