Created
December 6, 2021 09:59
-
-
Save ole/ad4dc93f9831dc471733cd2650cebc15 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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