Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Created September 1, 2022 09:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save swiftui-lab/e9b34a9a791f68ac1103a6d2889356f2 to your computer and use it in GitHub Desktop.
Save swiftui-lab/e9b34a9a791f68ac1103a6d2889356f2 to your computer and use it in GitHub Desktop.
A wave layout example
// Author: SwiftUI-Lab (swiftui-lab.com)
// Description: A demonstration of a wave Layout
// blog article: https://swiftui-lab.com/layout-protocol-part-2
import SwiftUI
struct ContentView: View {
@State var amplitude: CGFloat = 80
@State var phase: Double = 90.0
@State var frequency: Double = 2.0
let colors: [Color] = [.yellow, .orange, .red, .purple, .blue, .green]
var body: some View {
VStack {
Form {
Slider(value: $amplitude.animation(.spring()), in: 0...100) {
Text(String(format: "Amplitude = % 4.0f", amplitude))
}
Slider(value: $phase.animation(.spring()), in: 0...720) {
Text(String(format: " Phase = % 4.0f°", phase))
}
Slider(value: $frequency.animation(.spring()), in: 0...6) {
Text(String(format: "Frequency = %1.2f", frequency))
}
}
.formStyle(.grouped)
.frame(width: 400, height: 160)
.scrollContentBackground(.hidden)
Spacer()
WaveLayout(spacing: 5, amplitude: amplitude, frequency: frequency, phase: .degrees(phase)) {
ForEach(0..<24) { idx in
ContainerComponent {
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 30, height: 30)
.overlay { Text("\(idx+1)") }
}
}
}
.border(.gray)
Spacer()
}
.font(.custom("Menlo", size: 14))
.frame(maxWidth: .infinity)
}
}
struct ContainerComponent<V: View>: View {
@ViewBuilder let content: () -> V
@State private var rotation: Angle = .zero
var body: some View {
content()
.rotationEffect(rotation)
.layoutValue(key: Rotation.self, value: $rotation)
}
}
struct Rotation: LayoutValueKey {
static let defaultValue: Binding<Angle>? = nil
}
struct WaveLayout: Layout {
let spacing: CGFloat
var amplitude: CGFloat
var frequency: CGFloat
var phase: Angle
var animatableData: AnimatablePair<AnimatablePair<CGFloat,CGFloat>, Double> {
get {
AnimatablePair(AnimatablePair(amplitude, frequency), phase.radians)
}
set {
amplitude = newValue.first.first
frequency = newValue.first.second
phase = Angle.radians(newValue.second)
}
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
// Compute pt used for spacing, the tallest view and the total width used by views
let (accSpace, maxHeight, accWidth) = computeValues(subviews: subviews)
return CGSize(width: accSpace + accWidth, height: maxHeight + 2 * amplitude)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
// Compute pt used for spacing and the total width used by views
let (accSpace, _, accWidth) = computeValues(subviews: subviews)
// Computer the container's total width
let containerWidth = accSpace + accWidth
// Initialized subviews anchor point
var pt = CGPoint(x: bounds.minX, y: bounds.midY)
for v in subviews {
let viewSize = v.sizeThatFits(.unspecified)
// The sinusoid is: y = sin(x) where x is an angle. For example, if
// frequency = 2 and phase = 180°, then x would go from 180° to 900° (i.e., 180 + (2 x 360°))
let sinusoid_x = Angle.degrees(frequency * 360) * ((pt.x - bounds.minX) / containerWidth) + phase
let sinusoid_y = sin(sinusoid_x.radians)
pt.y = bounds.midY + amplitude * sinusoid_y
v.place(at: pt, anchor: .leading, proposal: .unspecified)
// Calculate view's rotation: The angle of the tangent of a curve,
// is calculated as the arctangent of the derivative of the function
let viewRotation = atan((((Angle.degrees(frequency * 360) * cos(sinusoid_x.radians))/containerWidth) * amplitude).radians)
DispatchQueue.main.async {
v[Rotation.self]?.wrappedValue = .radians(viewRotation)
}
// Advanced to next subview
pt.x += viewSize.width + spacing
}
}
func computeValues(subviews: Subviews) -> (accSpace: CGFloat, maxHeight: CGFloat, width: CGFloat) {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
let maxHeight = sizes.reduce(0) { max($0, $1.height) }
let accWidth = sizes.reduce(0) { $0 + $1.width }
let accSpace = spacing * CGFloat(subviews.count - 1)
return (accSpace, maxHeight, accWidth)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment