Forked from ryangittings/gist:e48a5eee26ce951125c86a2863917a15
Created
July 11, 2023 22:53
-
-
Save byaruhaf/5eeba4acae4ea85ed6eca3eacd28b122 to your computer and use it in GitHub Desktop.
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
// | |
// 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