Forked from swiftui-lab/advanced-swiftui-animations.swift
Created
September 6, 2019 15:36
-
-
Save mingsai/d831d81330a3554b41225da94ac0383e 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
//-------------------------------------------------- | |
// The SwiftUI Lab: Advanced SwiftUI Animations | |
// https://swiftui-lab.com/swiftui-animations-part1 | |
// https://swiftui-lab.com/swiftui-animations-part2 | |
//-------------------------------------------------- | |
import SwiftUI | |
struct ContentView: View { | |
var body: some View { | |
NavigationView { | |
List { | |
Section(header: Text("Part 1: Path Animations")) { | |
NavigationLink(destination: Example1(), label: { | |
Text("Example 1 (sides: Double)") | |
}) | |
NavigationLink(destination: Example2(), label: { | |
Text("Example 2 (sides: Int)") | |
}) | |
NavigationLink(destination: Example3(), label: { | |
Text("Example 3 (sides & scale)") | |
}) | |
NavigationLink(destination: Example4(), label: { | |
Text("Example 4 (vertex to vertex)") | |
}) | |
NavigationLink(destination: Example5(), label: { | |
Text("Example 5 (clock)") | |
}) | |
NavigationLink(destination: Example6(), label: { | |
Text("Example 6 (metal)") | |
}) | |
} | |
Section(header: Text("Part 2: Geometry Effect")) { | |
NavigationLink(destination: Example7(), label: { | |
Text("Example 7 (skew)") | |
}) | |
NavigationLink(destination: Example8(), label: { | |
Text("Example 8 (rotating card)") | |
}) | |
NavigationLink(destination: Example9(), label: { | |
Text("Example 9 (follow path)") | |
}) | |
} | |
Section(header: Text("Part 3: Animatable Modifier (coming soon)")) { | |
NavigationLink(destination: Text("COMING SOON"), label: { | |
Text("Example 10 (progress circle)") | |
}) | |
NavigationLink(destination: Text("COMING SOON"), label: { | |
Text("Example 11 (gradient)") | |
}) | |
NavigationLink(destination: Text("COMING SOON"), label: { | |
Text("Example 12 (wave text)") | |
}) | |
NavigationLink(destination: Text("COMING SOON"), label: { | |
Text("Example 13 (counter)") | |
}) | |
} | |
}.navigationBarTitle("SwiftUI Lab") | |
} | |
} | |
} | |
struct MyButton: View { | |
let label: String | |
var font: Font = .title | |
var textColor: Color = .white | |
let action: () -> () | |
var body: some View { | |
Button(action: { | |
self.action() | |
}, label: { | |
Text(label) | |
.font(font) | |
.padding(10) | |
.frame(width: 70) | |
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green).shadow(radius: 2)) | |
.foregroundColor(textColor) | |
}) | |
} | |
} | |
// MARK: - Part 1: Path Animations | |
// MARK: Example 1: Polygon animatable | |
struct Example1: View { | |
@State private var sides: Double = 4 | |
@State private var duration: Double = 1.0 | |
var body: some View { | |
VStack { | |
Example1PolygonShape(sides: sides) | |
.stroke(Color.blue, lineWidth: 3) | |
.padding(20) | |
.animation(.easeInOut(duration: duration)) | |
.layoutPriority(1) | |
Text("\(Int(sides)) sides").font(.headline) | |
HStack(spacing: 20) { | |
MyButton(label: "1") { | |
self.duration = self.animationTime(before: self.sides, after: 1) | |
self.sides = 1.0 | |
} | |
MyButton(label: "3") { | |
self.duration = self.animationTime(before: self.sides, after: 3) | |
self.sides = 3.0 | |
} | |
MyButton(label: "7") { | |
self.duration = self.animationTime(before: self.sides, after: 7) | |
self.sides = 7.0 | |
} | |
MyButton(label: "30") { | |
self.duration = self.animationTime(before: self.sides, after: 30) | |
self.sides = 30.0 | |
} | |
}.navigationBarTitle("Example 1").padding(.bottom, 50) | |
} | |
} | |
func animationTime(before: Double, after: Double) -> Double { | |
// Calculate an animation time that is | |
// adequate to the number of sides to add/remove. | |
return abs(before - after) * (1 / abs(before - after)) | |
} | |
} | |
struct Example1PolygonShape: Shape { | |
var sides: Double | |
var animatableData: Double { | |
get { return sides } | |
set { sides = newValue } | |
} | |
func path(in rect: CGRect) -> Path { | |
// hypotenuse | |
let h = Double(min(rect.size.width, rect.size.height)) / 2.0 | |
// center | |
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) | |
var path = Path() | |
let extra: Int = Double(sides) != Double(Int(sides)) ? 1 : 0 | |
for i in 0..<Int(sides) + extra { | |
let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180 | |
// Calculate vertex | |
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) | |
if i == 0 { | |
path.move(to: pt) // move to first vertex | |
} else { | |
path.addLine(to: pt) // draw line to next vertex | |
} | |
} | |
path.closeSubpath() | |
return path | |
} | |
} | |
// MARK: - Example 2: Polygon with sides as Integer | |
struct Example2: View { | |
@State private var sides: Int = 4 | |
@State private var duration: Double = 1.0 | |
var body: some View { | |
VStack { | |
Example2PolygonShape(sides: sides) | |
.stroke(Color.red, lineWidth: 3) | |
.padding(20) | |
.animation(.easeInOut(duration: duration)) | |
.layoutPriority(1) | |
Text("\(Int(sides)) sides").font(.headline) | |
HStack(spacing: 20) { | |
MyButton(label: "1") { | |
self.duration = self.animationTime(before: self.sides, after: 1) | |
self.sides = 1 | |
} | |
MyButton(label: "3") { | |
self.duration = self.animationTime(before: self.sides, after: 3) | |
self.sides = 3 | |
} | |
MyButton(label: "7") { | |
self.duration = self.animationTime(before: self.sides, after: 7) | |
self.sides = 7 | |
} | |
MyButton(label: "30") { | |
self.duration = self.animationTime(before: self.sides, after: 30) | |
self.sides = 30 | |
} | |
}.navigationBarTitle("Example 2").padding(.bottom, 50) | |
} | |
} | |
func animationTime(before: Int, after: Int) -> Double { | |
// Calculate an animation time that is | |
// adequate to the number of sides to add/remove. | |
return Double(abs(before - after)) * (1 / Double(abs(before - after))) | |
} | |
} | |
struct Example2PolygonShape: Shape { | |
var sides: Int | |
private var sidesAsDouble: Double | |
var animatableData: Double { | |
get { return sidesAsDouble } | |
set { sidesAsDouble = newValue } | |
} | |
init(sides: Int) { | |
self.sides = sides | |
self.sidesAsDouble = Double(sides) | |
} | |
func path(in rect: CGRect) -> Path { | |
// hypotenuse | |
let h = Double(min(rect.size.width, rect.size.height)) / 2.0 | |
// center | |
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) | |
var path = Path() | |
let extra: Int = sidesAsDouble != Double(Int(sidesAsDouble)) ? 1 : 0 | |
for i in 0..<Int(sidesAsDouble) + extra { | |
let angle = (Double(i) * (360.0 / sidesAsDouble)) * Double.pi / 180 | |
// Calculate vertex | |
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) | |
if i == 0 { | |
path.move(to: pt) // move to first vertex | |
} else { | |
path.addLine(to: pt) // draw line to next vertex | |
} | |
} | |
path.closeSubpath() | |
return path | |
} | |
} | |
// MARK: - Example 3: Polygon with multiple animatable paramters | |
struct Example3: View { | |
@State private var sides: Double = 4 | |
@State private var duration: Double = 1.0 | |
@State private var scale: Double = 1.0 | |
var body: some View { | |
VStack { | |
Example3PolygonShape(sides: sides, scale: scale) | |
.stroke(Color.purple, lineWidth: 5) | |
.padding(20) | |
.animation(.easeInOut(duration: duration)) | |
.layoutPriority(1) | |
Text("\(Int(sides)) sides, \(String(format: "%.2f", scale as Double)) scale") | |
HStack(spacing: 20) { | |
MyButton(label: "1") { | |
self.duration = self.animationTime(before: self.sides, after: 1) | |
self.sides = 1.0 | |
self.scale = 1.0 | |
} | |
MyButton(label: "3") { | |
self.duration = self.animationTime(before: self.sides, after: 3) | |
self.sides = 3.0 | |
self.scale = 0.7 | |
} | |
MyButton(label: "7") { | |
self.duration = self.animationTime(before: self.sides, after: 7) | |
self.sides = 7.0 | |
self.scale = 0.4 | |
} | |
MyButton(label: "30") { | |
self.duration = self.animationTime(before: self.sides, after: 30) | |
self.sides = 30.0 | |
self.scale = 1.0 | |
} | |
} | |
}.navigationBarTitle("Example 3").padding(.bottom, 50) | |
} | |
func animationTime(before: Double, after: Double) -> Double { | |
// Calculate an animation time that is | |
// adequate to the number of sides to add/remove. | |
return abs(before - after) * (1 / abs(before - after)) | |
} | |
} | |
struct Example3PolygonShape: Shape { | |
var sides: Double | |
var scale: Double | |
var animatableData: AnimatablePair<Double, Double> { | |
get { AnimatablePair(sides, scale) } | |
set { | |
sides = newValue.first | |
scale = newValue.second | |
} | |
} | |
func path(in rect: CGRect) -> Path { | |
// hypotenuse | |
let h = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale | |
// center | |
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) | |
var path = Path() | |
let extra: Int = sides != Double(Int(sides)) ? 1 : 0 | |
for i in 0..<Int(sides) + extra { | |
let angle = (Double(i) * (360.0 / sides)) * (Double.pi / 180) | |
// Calculate vertex | |
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) | |
if i == 0 { | |
path.move(to: pt) // move to first vertex | |
} else { | |
path.addLine(to: pt) // draw line to next vertex | |
} | |
} | |
path.closeSubpath() | |
return path | |
} | |
} | |
// MARK: - Example 4: Polygon with lines vertex-to-vertex | |
struct Example4: View { | |
@State private var sides: Double = 4 | |
@State private var duration: Double = 1.0 | |
@State private var scale: Double = 1.0 | |
var body: some View { | |
VStack { | |
Example4PolygonShape(sides: sides, scale: scale) | |
.stroke(Color.pink, lineWidth: (sides < 3) ? 10 : ( sides < 7 ? 5 : 2)) | |
.padding(20) | |
.animation(.easeInOut(duration: duration)) | |
.layoutPriority(1) | |
Text("\(Int(sides)) sides, \(String(format: "%.2f", scale as Double)) scale") | |
Slider(value: $sides, in: 0...30) | |
HStack(spacing: 20) { | |
MyButton(label: "1") { | |
self.duration = self.animationTime(before: self.sides, after: 1) | |
self.sides = 1.0 | |
self.scale = 1.0 | |
} | |
MyButton(label: "3") { | |
self.duration = self.animationTime(before: self.sides, after: 3) | |
self.sides = 3.0 | |
self.scale = 1.0 | |
} | |
MyButton(label: "7") { | |
self.duration = self.animationTime(before: self.sides, after: 7) | |
self.sides = 7.0 | |
self.scale = 1.0 | |
} | |
MyButton(label: "30") { | |
self.duration = self.animationTime(before: self.sides, after: 30) | |
self.sides = 30.0 | |
self.scale = 1.0 | |
} | |
} | |
}.navigationBarTitle("Example 4").padding(.bottom, 50) | |
} | |
func animationTime(before: Double, after: Double) -> Double { | |
// Calculate an animation time that is | |
// adequate to the number of sides to add/remove. | |
return abs(before - after) * (1 / abs(before - after)) + 3 | |
} | |
} | |
struct Example4PolygonShape: Shape { | |
var sides: Double | |
var scale: Double | |
var animatableData: AnimatablePair<Double, Double> { | |
get { AnimatablePair(sides, scale) } | |
set { | |
sides = newValue.first | |
scale = newValue.second | |
} | |
} | |
func path(in rect: CGRect) -> Path { | |
// hypotenuse | |
let h = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale | |
// center | |
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) | |
var path = Path() | |
let extra: Int = sides != Double(Int(sides)) ? 1 : 0 | |
var vertex: [CGPoint] = [] | |
for i in 0..<Int(sides) + extra { | |
let angle = (Double(i) * (360.0 / sides)) * (Double.pi / 180) | |
// Calculate vertex | |
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) | |
vertex.append(pt) | |
if i == 0 { | |
path.move(to: pt) // move to first vertex | |
} else { | |
path.addLine(to: pt) // draw line to next vertex | |
} | |
} | |
path.closeSubpath() | |
// Draw vertex-to-vertex lines | |
drawVertexLines(path: &path, vertex: vertex, n: 0) | |
return path | |
} | |
func drawVertexLines(path: inout Path, vertex: [CGPoint], n: Int) { | |
if (vertex.count - n) < 3 { return } | |
for i in (n+2)..<min(n + (vertex.count-1), vertex.count) { | |
path.move(to: vertex[n]) | |
path.addLine(to: vertex[i]) | |
} | |
drawVertexLines(path: &path, vertex: vertex, n: n+1) | |
} | |
} | |
// MARK: - Example 5: Clock Shape | |
struct Example5: View { | |
@State private var time: ClockTime = ClockTime(9, 50, 5) | |
@State private var duration: Double = 1.0 | |
var body: some View { | |
VStack { | |
ClockShape(clockTime: time) | |
.stroke(Color.blue, lineWidth: 3) | |
.padding(20) | |
.animation(.easeInOut(duration: duration)) | |
.layoutPriority(1) | |
Text("\(time.asString())") | |
HStack(spacing: 20) { | |
MyButton(label: "9:51:45", font: .footnote, textColor: .black) { | |
self.duration = 2.0 | |
self.time = ClockTime(9, 51, 45) | |
} | |
MyButton(label: "9:51:15", font: .footnote, textColor: .black) { | |
self.duration = 2.0 | |
self.time = ClockTime(9, 51, 15) | |
} | |
MyButton(label: "9:52:15", font: .footnote, textColor: .black) { | |
self.duration = 2.0 | |
self.time = ClockTime(9, 52, 15) | |
} | |
MyButton(label: "10:01:45", font: .caption, textColor: .black) { | |
self.duration = 10.0 | |
self.time = ClockTime(10, 01, 45) | |
} | |
} | |
}.navigationBarTitle("Example 5").padding(.bottom, 50) | |
} | |
} | |
struct ClockShape: Shape { | |
var clockTime: ClockTime | |
var animatableData: ClockTime { | |
get { clockTime } | |
set { clockTime = newValue } | |
} | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
let radius = min(rect.size.width / 2.0, rect.size.height / 2.0) | |
let center = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) | |
let hHypotenuse = Double(radius) * 0.5 // hour needle length | |
let mHypotenuse = Double(radius) * 0.7 // minute needle length | |
let sHypotenuse = Double(radius) * 0.9 // second needle length | |
let hAngle: Angle = .degrees(Double(clockTime.hours) / 12 * 360 - 90) | |
let mAngle: Angle = .degrees(Double(clockTime.minutes) / 60 * 360 - 90) | |
let sAngle: Angle = .degrees(Double(clockTime.seconds) / 60 * 360 - 90) | |
let hourNeedle = CGPoint(x: center.x + CGFloat(cos(hAngle.radians) * hHypotenuse), y: center.y + CGFloat(sin(hAngle.radians) * hHypotenuse)) | |
let minuteNeedle = CGPoint(x: center.x + CGFloat(cos(mAngle.radians) * mHypotenuse), y: center.y + CGFloat(sin(mAngle.radians) * mHypotenuse)) | |
let secondNeedle = CGPoint(x: center.x + CGFloat(cos(sAngle.radians) * sHypotenuse), y: center.y + CGFloat(sin(sAngle.radians) * sHypotenuse)) | |
path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) | |
path.move(to: center) | |
path.addLine(to: hourNeedle) | |
path = path.strokedPath(StrokeStyle(lineWidth: 3.0)) | |
path.move(to: center) | |
path.addLine(to: minuteNeedle) | |
path = path.strokedPath(StrokeStyle(lineWidth: 3.0)) | |
path.move(to: center) | |
path.addLine(to: secondNeedle) | |
path = path.strokedPath(StrokeStyle(lineWidth: 1.0)) | |
return path | |
} | |
} | |
struct ClockTime { | |
var hours: Int // Hour needle should jump by integer numbers | |
var minutes: Int // Minute needle should jump by integer numbers | |
var seconds: Double // Second needle should move smoothly | |
// Initializer with hour, minute and seconds | |
init(_ h: Int, _ m: Int, _ s: Double) { | |
self.hours = h | |
self.minutes = m | |
self.seconds = s | |
} | |
// Initializer with total of seconds | |
init(_ seconds: Double) { | |
let h = Int(seconds) / 3600 | |
let m = (Int(seconds) - (h * 3600)) / 60 | |
let s = seconds - Double((h * 3600) + (m * 60)) | |
self.hours = h | |
self.minutes = m | |
self.seconds = s | |
} | |
// compute number of seconds | |
var asSeconds: Double { | |
return Double(self.hours * 3600 + self.minutes * 60) + self.seconds | |
} | |
// show as string | |
func asString() -> String { | |
return String(format: "%2i", self.hours) + ":" + String(format: "%02i", self.minutes) + ":" + String(format: "%02.0f", self.seconds) | |
} | |
} | |
extension ClockTime: VectorArithmetic { | |
static func -= (lhs: inout ClockTime, rhs: ClockTime) { | |
lhs = lhs - rhs | |
} | |
static func - (lhs: ClockTime, rhs: ClockTime) -> ClockTime { | |
return ClockTime(lhs.asSeconds - rhs.asSeconds) | |
} | |
static func += (lhs: inout ClockTime, rhs: ClockTime) { | |
lhs = lhs + rhs | |
} | |
static func + (lhs: ClockTime, rhs: ClockTime) -> ClockTime { | |
return ClockTime(lhs.asSeconds + rhs.asSeconds) | |
} | |
mutating func scale(by rhs: Double) { | |
var s = Double(self.asSeconds) | |
s.scale(by: rhs) | |
let ct = ClockTime(s) | |
self.hours = ct.hours | |
self.minutes = ct.minutes | |
self.seconds = ct.seconds | |
} | |
var magnitudeSquared: Double { | |
1 | |
} | |
static var zero: ClockTime { | |
return ClockTime(0, 0, 0) | |
} | |
} | |
// MARK: - Example 6: Clock Shape | |
struct Example6: View { | |
var body: some View { | |
VStack { | |
FlowerView().drawingGroup() | |
}.padding(20) | |
} | |
} | |
struct FlowerView: View { | |
@State private var animate = false | |
let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple, .pink] | |
var body: some View { | |
ZStack { | |
ForEach(0..<7) { i in | |
FlowerColor(petals: self.getPetals(i), length: self.getLength(i), color: self.colors[i]) | |
} | |
.rotationEffect(Angle(degrees: animate ? 360 : 0)) | |
.onAppear { | |
withAnimation(Animation.easeInOut(duration: 25.0).repeatForever()) { | |
self.animate = true | |
} | |
} | |
} | |
} | |
func getLength(_ i: Int) -> Double { | |
return 1 - (Double(i) * 1 / 7) | |
} | |
func getPetals(_ i: Int) -> Int { | |
return i * 2 + 15 | |
} | |
} | |
struct FlowerColor: View { | |
let petals: Int | |
let length: Double | |
let color: Color | |
@State private var animate = false | |
var body: some View { | |
let petalWidth1 = Angle(degrees: 2) | |
let petalWidth2 = Angle(degrees: 360 / Double(self.petals)) * 2 | |
return GeometryReader { proxy in | |
ForEach(0..<self.petals) { i in | |
PetalShape(angle: Angle(degrees: Double(i) * 360 / Double(self.petals)), arc: self.animate ? petalWidth1 : petalWidth2, length: self.animate ? self.length : self.length * 0.9) | |
.fill(RadialGradient(gradient: Gradient(colors: [self.color.opacity(0.2), self.color]), center: UnitPoint(x: 0.5, y: 0.5), startRadius: 0.1 * min(proxy.size.width, proxy.size.height) / 2.0, endRadius: min(proxy.size.width, proxy.size.height) / 2.0)) | |
} | |
}.onAppear { | |
withAnimation(Animation.easeInOut(duration: 1.5).repeatForever()) { | |
self.animate = true | |
} | |
} | |
} | |
} | |
struct PetalShape: Shape { | |
let angle: Angle | |
var arc: Angle | |
var length: Double | |
var animatableData: AnimatablePair<Double, Double> { | |
get { AnimatablePair(arc.degrees, length) } | |
set { | |
arc = Angle(degrees: newValue.first) | |
length = newValue.second | |
} | |
} | |
func path(in rect: CGRect) -> Path { | |
let center = CGPoint(x: rect.midX, y: rect.midY) | |
let hypotenuse = Double(min(rect.width, rect.height)) / 2.0 * length | |
let sep = arc / 2 | |
let to = CGPoint(x: CGFloat(cos(angle.radians) * Double(hypotenuse)) + center.x, | |
y: CGFloat(sin(angle.radians) * Double(hypotenuse)) + center.y) | |
let ctrl1 = CGPoint(x: CGFloat(cos((angle + sep).radians) * Double(hypotenuse)) + center.x, | |
y: CGFloat(sin((angle + sep).radians) * Double(hypotenuse)) + center.y) | |
let ctrl2 = CGPoint(x: CGFloat(cos((angle - sep).radians) * Double(hypotenuse)) + center.x, | |
y: CGFloat(sin((angle - sep).radians) * Double(hypotenuse)) + center.y) | |
var path = Path() | |
path.move(to: center) | |
path.addQuadCurve(to: to, control: ctrl1) | |
path.addQuadCurve(to: center, control: ctrl2) | |
return path | |
} | |
} | |
// MARK: - | |
// MARK: Part 2: Geometry Effects | |
// MARK: Exmaple 6 - Skew | |
struct Example7: View { | |
@State private var moveIt = false | |
var body: some View { | |
let animation = Animation.easeInOut(duration: 1.0) | |
return VStack { | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .red) | |
.animation(animation) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .orange) | |
.animation(animation.delay(0.1)) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .yellow) | |
.animation(animation.delay(0.2)) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .green) | |
.animation(animation.delay(0.3)) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .blue) | |
.animation(animation.delay(0.4)) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .purple) | |
.animation(animation.delay(0.5)) | |
LabelView(text: "The SwiftUI Lab", offset: moveIt ? 120 : -120, pct: moveIt ? 1 : 0, backgroundColor: .pink) | |
.animation(animation.delay(0.6)) | |
Button(action: { self.moveIt.toggle() }) { Text("Animate") }.padding(.top, 50) | |
} | |
.onTapGesture { self.moveIt.toggle() } | |
.navigationBarTitle("Example 7") | |
} | |
} | |
struct LabelView: View { | |
let text: String | |
var offset: CGFloat | |
var pct: CGFloat | |
let backgroundColor: Color | |
var body: some View { | |
Text("The SwiftUI Lab") | |
.font(.headline) | |
.padding(5) | |
.background(RoundedRectangle(cornerRadius: 5).foregroundColor(backgroundColor)) | |
.foregroundColor(Color.black) | |
.modifier(SkewedOffset(offset: offset, pct: pct, goingRight: offset > 0)) | |
} | |
} | |
struct SkewedOffset: GeometryEffect { | |
var offset: CGFloat | |
var pct: CGFloat | |
let goingRight: Bool | |
init(offset: CGFloat, pct: CGFloat, goingRight: Bool) { | |
self.offset = offset | |
self.pct = pct | |
self.goingRight = goingRight | |
} | |
var animatableData: AnimatablePair<CGFloat, CGFloat> { | |
get { return AnimatablePair<CGFloat, CGFloat>(offset, pct) } | |
set { | |
offset = newValue.first | |
pct = newValue.second | |
} | |
} | |
func effectValue(size: CGSize) -> ProjectionTransform { | |
var skew: CGFloat | |
if pct < 0.2 { | |
skew = (pct * 5) * 0.5 * (goingRight ? -1 : 1) | |
} else if pct > 0.8 { | |
skew = ((1 - pct) * 5) * 0.5 * (goingRight ? -1 : 1) | |
} else { | |
skew = 0.5 * (goingRight ? -1 : 1) | |
} | |
return ProjectionTransform(CGAffineTransform(a: 1, b: 0, c: skew, d: 1, tx: offset, ty: 0)) | |
} | |
} | |
// MARK: - Example 8 - Rotating Card | |
struct Example8: View { | |
var body: some View { | |
HStack { | |
Spacer() | |
RotatingCard() | |
Spacer() | |
}.background(Color.black).navigationBarTitle("Example 8") | |
} | |
} | |
struct RotatingCard: View { | |
@State private var flipped = false | |
@State private var animate3d = false | |
@State private var rotate = false | |
@State private var imgIndex = 0 | |
let images = ["diamonds-7", "clubs-8", "diamonds-6", "clubs-b", "hearts-2", "diamonds-b"] | |
var body: some View { | |
let binding = Binding<Bool>(get: { self.flipped }, set: { self.updateBinding($0) }) | |
return VStack { | |
Spacer() | |
Image(flipped ? "back" : images[imgIndex]).resizable() | |
.frame(width: 212, height: 320) | |
.modifier(FlipEffect(flipped: binding, angle: animate3d ? 360 : 0, axis: (x: 1, y: 5))) | |
.rotationEffect(Angle(degrees: rotate ? 0 : 360)) | |
.onAppear { | |
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) { | |
self.animate3d = true | |
} | |
withAnimation(Animation.linear(duration: 8.0).repeatForever(autoreverses: false)) { | |
self.rotate = true | |
} | |
} | |
Spacer() | |
} | |
} | |
func updateBinding(_ value: Bool) { | |
// If card was just flipped and at front, change the card | |
if flipped != value && !flipped { | |
self.imgIndex = self.imgIndex+1 < self.images.count ? self.imgIndex+1 : 0 | |
} | |
flipped = value | |
} | |
} | |
struct FlipEffect: GeometryEffect { | |
var animatableData: Double { | |
get { angle } | |
set { angle = newValue } | |
} | |
@Binding var flipped: Bool | |
var angle: Double | |
let axis: (x: CGFloat, y: CGFloat) | |
func effectValue(size: CGSize) -> ProjectionTransform { | |
// We schedule the change to be done after the view has finished drawing, | |
// otherwise, we would receive a runtime error, indicating we are changing | |
// the state while the view is being drawn. | |
DispatchQueue.main.async { | |
self.flipped = self.angle >= 90 && self.angle < 270 | |
} | |
let a = CGFloat(Angle(degrees: angle).radians) | |
var transform3d = CATransform3DIdentity; | |
transform3d.m34 = -1/max(size.width, size.height) | |
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0) | |
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0) | |
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0)) | |
return ProjectionTransform(transform3d).concatenating(affineTransform) | |
} | |
} | |
// MARK: - Example 9 - Follow Path | |
struct Example9: View { | |
@State private var flag = false | |
var body: some View { | |
GeometryReader { proxy in | |
ZStack(alignment: .topLeading) { | |
// Draw the Infinity Shape | |
InfinityShape().stroke(Color.purple, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [7, 7], dashPhase: 0)) | |
.foregroundColor(.blue) | |
.frame(width: proxy.size.width, height: 300) | |
// Animate movement of Image | |
Image(systemName: "airplane").resizable().foregroundColor(Color.red) | |
.frame(width: 50, height: 50).offset(x: -25, y: -25) | |
.modifier(FollowEffect(pct: self.flag ? 1 : 0, path: InfinityShape.createInfinityPath(in: CGRect(x: 0, y: 0, width: proxy.size.width, height: 300)), rotate: true)) | |
.onAppear { | |
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) { | |
self.flag.toggle() | |
} | |
} | |
}.frame(alignment: .topLeading) | |
} | |
.padding(20) | |
.navigationBarTitle("Example 9") | |
} | |
} | |
struct FollowEffect: GeometryEffect { | |
var pct: CGFloat = 0 | |
let path: Path | |
var rotate = true | |
var animatableData: CGFloat { | |
get { return pct } | |
set { pct = newValue } | |
} | |
func effectValue(size: CGSize) -> ProjectionTransform { | |
if !rotate { | |
let pt = percentPoint(pct) | |
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)) | |
} else { | |
// Calculate rotation angle, by calculating an imaginary line between two points | |
// in the path: the current position (1) and a point very close behind in the path (2). | |
let pt1 = percentPoint(pct) | |
let pt2 = percentPoint(pct - 0.01) | |
let a = pt2.x - pt1.x | |
let b = pt2.y - pt1.y | |
let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi | |
let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle)) | |
return ProjectionTransform(transform) | |
} | |
} | |
func percentPoint(_ percent: CGFloat) -> CGPoint { | |
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) | |
let f = pct > 0.999 ? CGFloat(1-0.001) : pct | |
let t = pct > 0.999 ? CGFloat(1) : pct + 0.001 | |
let tp = path.trimmedPath(from: f, to: t) | |
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY) | |
} | |
} | |
struct InfinityShape: Shape { | |
func path(in rect: CGRect) -> Path { | |
return InfinityShape.createInfinityPath(in: rect) | |
} | |
static func createInfinityPath(in rect: CGRect) -> Path { | |
let height = rect.size.height | |
let width = rect.size.width | |
let heightFactor = height/4 | |
let widthFactor = width/4 | |
var path = Path() | |
path.move(to: CGPoint(x:widthFactor, y: heightFactor * 3)) | |
path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor), control1: CGPoint(x:0, y: heightFactor * 3), control2: CGPoint(x:0, y: heightFactor)) | |
path.move(to: CGPoint(x:widthFactor, y: heightFactor)) | |
path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3), control1: CGPoint(x:widthFactor * 2, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) | |
path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3)) | |
path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor), control1: CGPoint(x:widthFactor * 4 + 5, y: heightFactor * 3), control2: CGPoint(x:widthFactor * 4 + 5, y: heightFactor)) | |
path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor)) | |
path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor * 3), control1: CGPoint(x:widthFactor * 2, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) | |
return path | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment