Created
June 8, 2021 22:58
-
-
Save zarghol/3c0f29bdbe7a6317536bf0a8c76a7380 to your computer and use it in GitHub Desktop.
Experiment with new SwiftUI Canvas View
This file contains hidden or 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
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