Skip to content

Instantly share code, notes, and snippets.

@daltonclaybrook
Created August 25, 2019 00:03
Show Gist options
  • Save daltonclaybrook/414e732ef9a46ded6eb5b50319f8e5e9 to your computer and use it in GitHub Desktop.
Save daltonclaybrook/414e732ef9a46ded6eb5b50319f8e5e9 to your computer and use it in GitHub Desktop.
import Combine
import SwiftUI
extension Publisher {
func withPrevious(_ initial: Output) -> AnyPublisher<(Output, Output), Failure> {
scan((initial, initial)) { previousTuple, currentValue in
(previousTuple.1, currentValue)
}.eraseToAnyPublisher()
}
}
extension View {
func radialOffset(angle: CGFloat, radius: CGFloat) -> some View {
offset(
x: radius * cos(angle),
y: radius * sin(angle)
)
}
}
import Combine
import SwiftUI
import WatchKit
private final class LoadingViewModel: ObservableObject {
@Published private(set) var radius: CGFloat = 0
@Published private(set) var angle = Angle(radians: 0)
let dotDimension: CGFloat = 4
var viewWidth: CGFloat {
maxRadius * 2 + dotDimension
}
private let minRadius: CGFloat = 14
private let maxRadius: CGFloat = 26
private let sinMultiplier: Double = 2 * .pi / 3
private let slowAngleMultiplier: Double = 2 * .pi / 5
private let fastAngleMultiplier: Double = 2 * .pi / 1.5
private var timerCancellable: AnyCancellable?
func startAnimating() {
timerCancellable?.cancel()
let startDate = Date()
timerCancellable = Timer
.publish(every: 1 / 60, on: .main, in: .common)
.autoconnect()
.withPrevious(Date())
.sink { [weak self] dates in
let elapsed = dates.1.timeIntervalSince(startDate)
let delta = dates.1.timeIntervalSince(dates.0)
self?.advanceWith(elapsedTime: elapsed, delta: delta)
}
}
func stopAnimating() {
timerCancellable?.cancel()
}
// MARK: - Helpers
private func advanceWith(elapsedTime: TimeInterval, delta: TimeInterval) {
let sinCalc = (sin(elapsedTime * sinMultiplier) + 1.0) / 2.0
let angleMultiplier = (slowAngleMultiplier - fastAngleMultiplier) * sinCalc + fastAngleMultiplier
let newAngle = angle.radians + delta * angleMultiplier
angle = Angle(radians: newAngle)
radius = (maxRadius - minRadius) * CGFloat(sinCalc) + minRadius
}
}
struct LoadingView: View {
@ObservedObject private var viewModel = LoadingViewModel()
var dotCount = 10
var color = Color.white
var body: some View {
ZStack {
ForEach(0..<dotCount) { dotIndex in
Circle()
.fill(self.color)
.frame(width: self.viewModel.dotDimension, height: self.viewModel.dotDimension)
.radialOffset(
angle: self.angleFor(index: dotIndex),
radius: self.viewModel.radius
)
}
}.frame(width: viewModel.viewWidth, height: viewModel.viewWidth)
.rotationEffect(viewModel.angle)
.onAppear { self.viewModel.startAnimating() }
.onDisappear { self.viewModel.stopAnimating() }
}
// MARK: - Helpers
private func angleFor(index: Int) -> CGFloat {
2 * .pi / CGFloat(dotCount) * CGFloat(index)
}
}
#if DEBUG
struct LoadingView_Previews: PreviewProvider {
static var previews: some View {
LoadingView()
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment