Skip to content

Instantly share code, notes, and snippets.

@zarghol
Created June 8, 2021 22:58
Show Gist options
  • Save zarghol/3c0f29bdbe7a6317536bf0a8c76a7380 to your computer and use it in GitHub Desktop.
Save zarghol/3c0f29bdbe7a6317536bf0a8c76a7380 to your computer and use it in GitHub Desktop.
Experiment with new SwiftUI Canvas View
import SwiftUI
enum TrigonometricFunction: CaseIterable {
case sinus
case cosinus
func apply(_ value: Double) -> Double {
switch self {
case .cosinus:
return cos(value)
case .sinus:
return sin(value)
}
}
}
extension CirclesModel {
struct StaticParameters {
// functions called to trim the displayed path
let fromFunction: TrigonometricFunction
let toFunction: TrigonometricFunction
// the timer of the animation in seconds
let timer: Int
// a variation in case the from and to functions are the same, and too add some spice to the animation
let angularVariation: Int
static func random() -> Self {
let from = TrigonometricFunction.allCases.randomElement()!
let to = TrigonometricFunction.allCases.randomElement()!
let timer = Array(3...8).randomElement()!
let angularVariation = Array(0...360).randomElement()!
return StaticParameters(
fromFunction: from,
toFunction: to,
timer: timer,
angularVariation: angularVariation
)
}
}
struct Output {
let strokeStart: Double
let strokeEnd: Double
}
}
final class CirclesModel: ObservableObject {
let parameters: [StaticParameters]
let circleNumber: Int
@Published var isPaused: Bool = false
var currentDate = Date()
var interval: Double = 0.0
init(circleNumber: Int) {
self.circleNumber = circleNumber
self.parameters = (0..<circleNumber).map { _ in StaticParameters.random() }
}
func output(forCircleAtIndex index: Int, currentDate: Date) -> Output {
defer { self.currentDate = currentDate }
let parameters = self.parameters[index]
let seconds = currentDate.timeIntervalSinceReferenceDate - interval
let cycles = 360 / parameters.timer
let angle = Angle.degrees(seconds.remainder(dividingBy: Double(parameters.timer)) * Double(cycles))
let variatedToAngle = angle - Angle.degrees(Double(parameters.angularVariation))
self.currentDate = currentDate
return Output(
strokeStart: parameters.fromFunction.apply(angle.radians),
strokeEnd: parameters.toFunction.apply(variatedToAngle.radians)
)
}
func togglePause() {
isPaused.toggle()
if !isPaused {
self.interval += Date().timeIntervalSinceReferenceDate - currentDate.timeIntervalSinceReferenceDate
}
}
}
struct CirclesLoader: View {
@StateObject private var viewModel: CirclesModel
init(circleNumber: Int) {
_viewModel = StateObject(wrappedValue: CirclesModel(circleNumber: circleNumber))
}
var body: some View {
TimelineView(.animation(paused: viewModel.isPaused)) { timeline in
Canvas { context, size in
let rect = CGRect(
origin: .zero,
size: size
)
for i in 0..<viewModel.circleNumber {
let circleRect = rect.insetBy(
dx: CGFloat(i * 20),
dy: CGFloat(i * 20)
)
let output = viewModel.output(forCircleAtIndex: i, currentDate: timeline.date)
let path = Circle()
.path(in: circleRect)
.trimmedPath(
from: output.strokeStart,
to: output.strokeEnd
)
context.stroke(
path,
with: .foreground,
style: StrokeStyle(
lineWidth: 4,
lineCap: .round,
lineJoin: .round
)
)
}
}
}
.accessibility(label: Text("A Circle loader"))
.onTapGesture { viewModel.togglePause() }
.padding()
}
}
// Usage of the CirclesLoader, and main view of the experiment project
struct ContentView: View {
var body: some View {
CirclesLoader(circleNumber: 8)
.foregroundStyle(.linearGradient(
colors: [.red, .purple],
startPoint: UnitPoint.leading,
endPoint: UnitPoint.trailing
))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment