Created
August 30, 2022 07:27
-
-
Save swiftui-lab/3482055332763035e603b22e9c5754fb to your computer and use it in GitHub Desktop.
This file contains 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
// 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