Skip to content

Instantly share code, notes, and snippets.

@ole
Created December 6, 2021 09:59
Show Gist options
  • Save ole/ad4dc93f9831dc471733cd2650cebc15 to your computer and use it in GitHub Desktop.
Save ole/ad4dc93f9831dc471733cd2650cebc15 to your computer and use it in GitHub Desktop.
// A view that can flip between its "front" and "back" side.
//
// Animation implementation based on:
// Chris Eidhof, Keyframe animations <https://gist.github.com/chriseidhof/ea0e435197f550b195bb749f4777bbf7>
import SwiftUI
// MARK: - Chris's keyframe animation design
struct Keyframe<Data: Animatable> {
init(position: Double, _ data: Data) {
self.position = position
self.data = data
}
var position: Double
var data: Data
}
fileprivate struct Helper<Data: Animatable, Body: View>: AnimatableModifier {
var keyFrames: [Keyframe<Data>]
let runBody: (Data) -> Body
var animatableData: CGFloat
var currentValue: Data {
for ix in keyFrames.indices.dropLast() {
let frame = keyFrames[ix]
let next = keyFrames[ix + 1]
assert(frame.position < next.position, "Invalid keyframe position")
if frame.position <= animatableData, next.position >= animatableData {
let amount = (animatableData - frame.position) / (next.position - frame.position)
let data0 = frame.data.animatableData
let data1 = next.data.animatableData
var diff = data1 - data0
diff.scale(by: amount)
var result = frame.data
result.animatableData = (data0 + diff)
return result
}
}
return keyFrames.last!.data
}
func body(content: Content) -> some View {
runBody(currentValue)
}
}
struct KeyFrameAnimation<Data: Animatable, Content: View>: View {
var keyFrames: [Keyframe<Data>]
var completion: CGFloat // should be between 0 and 1
var content: (Data) -> Content
var body: some View {
EmptyView().modifier(Helper(keyFrames: keyFrames, runBody: content, animatableData: completion))
}
}
// MARK: - Flip view
// This type holds the properties that we want to animate. We could also specify "raw" animatable data instead,
// but the nice thing about this is that it lets us have "semantic" names for the properties.
struct FlipRotation: Animatable {
var angle: Angle
var animatableData: Double {
get { angle.degrees }
set { angle = .degrees(newValue) }
}
}
/// - TODO: Make the content views for front and back sides configurable by the client.
struct FlipView: View {
@State var isFlipped = false
let frontKeyframes = [
Keyframe(position: 0, FlipRotation(angle: .zero)),
Keyframe(position: 0.5, FlipRotation(angle: .degrees(-90))),
Keyframe(position: 1.0, FlipRotation(angle: .degrees(-90))),
]
let backKeyframes = [
Keyframe(position: 0, FlipRotation(angle: .degrees(90))),
Keyframe(position: 0.5, FlipRotation(angle: .degrees(90))),
Keyframe(position: 1.0, FlipRotation(angle: .degrees(0))),
]
var body: some View {
ZStack {
KeyFrameAnimation(keyFrames: frontKeyframes, completion: isFlipped ? 1 : 0) { value in
// Front side
RoundedRectangle(cornerRadius: 16)
.fill(LinearGradient(colors: [.green, .cyan], startPoint: .topLeading, endPoint: .bottomTrailing))
.overlay {
Text("Front").foregroundStyle(.white)
}
.rotation3DEffect(value.angle, axis: (x: 1, y: 0, z: 0), perspective: 0.3)
}
KeyFrameAnimation(keyFrames: backKeyframes, completion: isFlipped ? 1 : 0) { value in
// Back side
RoundedRectangle(cornerRadius: 16)
.fill(LinearGradient(colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing))
.overlay {
Text("Back").foregroundStyle(.black)
}
.rotation3DEffect(value.angle, axis: (x: 1, y: 0, z: 0), perspective: 0.3)
}
}
.font(.system(size: 64, design: .rounded).bold())
.frame(width: 300, height: 300)
.padding()
.onTapGesture {
withAnimation(.easeInOut(duration: 0.5)) {
isFlipped.toggle()
}
}
}
}
struct ContentView: View {
var body: some View {
FlipView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment