Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mingsai/d831d81330a3554b41225da94ac0383e to your computer and use it in GitHub Desktop.
Save mingsai/d831d81330a3554b41225da94ac0383e to your computer and use it in GitHub Desktop.
//--------------------------------------------------
// 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