Daniel
Daniel

Reputation: 3597

Interactive UIView flip transition

I have the front and the back of a card. I animate the transition between the two like this:

private func flipToBack() {
    UIView.transition(from: frontContainer, to: backContainer, duration: 0.5, options: [.transitionFlipFromRight, .showHideTransitionViews], completion: nil)
}

private func flipToFront() {
    UIView.transition(from: backContainer, to: frontContainer, duration: 0.5, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
}

This works perfectly. However, I want to make this animation interactive, so that if the user pans horizontally across the card, the flip animation will advance proportionally. Usually, I would do this kind of interactive animation with a UIViewPropertyAnimator, but I do not know what property I would animate in the animator without building up the flip animation from scratch.

Is it possible to use UIViewPropertyAnimator, or is there some other alternative to make the flip interactive?

Upvotes: 4

Views: 1014

Answers (1)

Daniel
Daniel

Reputation: 3597

I ended up writing it myself. The code is pretty long, so here's a link to the full program on GitHub. Here are the key parts:

Everything is encapsulated in an InteractiveFlipAnimator object that takes a front view (v1) and a back view (v2). Each view also gets a black cover that functions as a shadow to add that darkening effect when the view turns in perspective.

Here is the panning function:

/// Add a `UIPanGestureRecognizer` to the main view that contains the card and pass it onto this function.
@objc func pan(_ gesture: UIPanGestureRecognizer) {
    guard let view = gesture.view else { return }
    if isAnimating { return }

    let translation = gesture.translation(in: view)
    let x = translation.x
    let angle = startAngle + CGFloat.pi * x / view.frame.width

    // If the angle is outside [-pi, 0], then do not rotate the view and count it as touchesEnded. This works because the full width is the screen width.
    if angle < -CGFloat.pi || angle > 0 {
        if gesture.state != .began && gesture.state != .changed {
            finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
        }
        return
    }

    var transform = CATransform3DIdentity
    // Perspective transform
    transform.m34 = 1 / -500
    // y rotation transform
    transform = CATransform3DRotate(transform, angle, 0, 1, 0)
    self.v1.layer.transform = transform
    self.v2.layer.transform = transform

    // Set the shadow
    if startAngle == 0 {
        self.v1Cover.alpha = 1 - abs(x / view.frame.width)
        self.v2Cover.alpha = abs(x / view.frame.width)
    } else {
        self.v1Cover.alpha = abs(x / view.frame.width)
        self.v2Cover.alpha = 1 - abs(x / view.frame.width)
    }

    // Set which view is on top. This flip happens when it looks like the two views make a vertical line.
    if abs(angle) < CGFloat.pi / 2 {
        // Flipping point
        v1.layer.zPosition = 0
        v2.layer.zPosition = 1
    } else {
        v1.layer.zPosition = 1
        v2.layer.zPosition = 0
    }

    // Save state
    if gesture.state != .began && gesture.state != .changed {
        finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
    }
}

The code to finish panning is very similar, but it is also much longer. To see it all come together, visit the GitHub link above.

Upvotes: 5

Related Questions