Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryangittings/e48a5eee26ce951125c86a2863917a15 to your computer and use it in GitHub Desktop.
Save ryangittings/e48a5eee26ce951125c86a2863917a15 to your computer and use it in GitHub Desktop.
//
// WheelView.swift
// ShowcaseShareCard
//
// Created by Ryan Gittings on 10/07/2023.
//
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.yellow, .orange, .red, .purple, .blue, .green]
@State var angle: Angle = .zero
@State var radius: CGFloat = 140.0
@State var animation: Animation? = nil
var body: some View {
VStack {
Spacer()
Wheel(radius: radius, rotation: angle) {
contents()
}
.background(Color.orange)
Spacer()
}
.background(.white)
}
@ViewBuilder func contents(animation: Animation? = nil) -> some View {
ForEach(0..<10) { idx in
WheelComponent(animation: animation) {
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count])
.frame(width: 70, height: 105)
.overlay {
Text("\(idx+1)")
}
}
}
}
}
struct Rotation: LayoutValueKey {
static let defaultValue: Binding<Angle>? = nil
}
struct WheelComponent<V: View>: View {
var animation: Animation? = nil
@ViewBuilder let content: () -> V
@State private var rotation: Angle = .zero
var body: some View {
content()
.rotationEffect(rotation)
.layoutValue(key: Rotation.self, value: $rotation.animation(animation))
}
}
struct Wheel: Layout {
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get {
AnimatablePair(rotation.radians, radius)
}
set {
rotation = Angle.radians(newValue.first)
radius = newValue.second
}
}
var radius: CGFloat
var rotation: Angle
private static let arcDegrees: CGFloat = 60
private static let arcRadians = (arcDegrees * CGFloat.pi) / 180
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) { CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) }
return CGSize(width: (maxSize.width / 2 + radius) * 2, height: maxSize.height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
for (index, subview) in subviews.enumerated() {
let fanRadius = fanRadius(bounds: bounds, proposal: proposal)
let angle = angleForCard(n: index, subviews: subviews)
var point = CGPoint(x: sin(angle) * fanRadius, y: 0)
point.x += bounds.midX
point.y += bounds.midY
subview.place(at: point, anchor: .center, proposal: .unspecified)
DispatchQueue.main.async {
subview[Rotation.self]?.wrappedValue = .radians(angle)
}
}
}
func fanRadius(bounds: CGRect, proposal: ProposedViewSize) -> CGFloat {
let sinAngle = sin(Wheel.arcRadians / 2.0)
let availableWidth = (bounds.size.width - (proposal.width ?? 0)) / 2.0
return sinAngle == 0 ? availableWidth : availableWidth / sinAngle
}
func angleForCard(n: Int, subviews: Subviews) -> CGFloat {
let nGaps = max(CGFloat(subviews.count) - 1, 1)
let fraction = (CGFloat(n) - (nGaps / 2)) / nGaps
return fraction * -Wheel.arcRadians
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment