Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Created August 30, 2022 07:27
Show Gist options
  • Save swiftui-lab/3482055332763035e603b22e9c5754fb to your computer and use it in GitHub Desktop.
Save swiftui-lab/3482055332763035e603b22e9c5754fb to your computer and use it in GitHub Desktop.
// Author: SwiftUI-Lab (swiftui-lab.com)
// Description: A demonstration of a Layout with custom animations
// and bi-directional layout values.
// blog article: https://swiftui-lab.com/layout-protocol-part-2
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()
HStack(spacing: 15) {
Wheel(radius: radius, rotation: angle, pointToCenter: false) {
contents()
}
Wheel(radius: radius, rotation: angle, pointToCenter: true) {
contents()
}
Wheel(radius: radius, rotation: angle, pointToCenter: true) {
contents(animation: animation)
}
}
.onAppear {
// Set the view rotation animation after the view appeared,
// to avoid animating initial rotation
DispatchQueue.main.async {
animation = .easeInOut(duration: 1.0)
}
}
Spacer()
Button("Rotate") {
withAnimation(.easeInOut(duration: 4.0)) {
angle = (angle == .zero ? .degrees(360) : .zero)
}
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
}
@ViewBuilder func contents(animation: Animation? = nil) -> some View {
ForEach(0..<12) { idx in
WheelComponent(animation: animation) {
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 70, height: 70)
.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
var pointToCenter = false
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) {
return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height))
}
return CGSize(width: (maxSize.width / 2 + radius) * 2,
height: (maxSize.height / 2 + radius) * 2)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let angleStep = (Angle.degrees(360).radians / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = angleStep * CGFloat(index) + rotation.radians
// Find a vector with an appropriate size and rotation.
var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle))
// Shift the vector to the middle of the region.
point.x += bounds.midX
point.y += bounds.midY
// Place the subview.
subview.place(at: point, anchor: .center, proposal: .unspecified)
DispatchQueue.main.async {
if pointToCenter {
subview[Rotation.self]?.wrappedValue = .radians(angle)
} else {
subview[Rotation.self]?.wrappedValue = .zero
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment