Skip to content

Instantly share code, notes, and snippets.

@Koshimizu-Takehito
Last active October 26, 2025 14:21
Show Gist options
  • Save Koshimizu-Takehito/b6993e24c638b1ede25ffb0f6e1cc72a to your computer and use it in GitHub Desktop.
Save Koshimizu-Takehito/b6993e24c638b1ede25ffb0f6e1cc72a to your computer and use it in GitHub Desktop.
辺の比がフィボナッチ数の正方形になるように配置するLayoutプロトコル
import SwiftUI
struct FibonacciLayout: Layout {
struct Cache {
var fib: [Int128]
}
func makeCache(subviews: Subviews) -> Cache {
switch subviews.count {
case ...2:
return Cache(fib: Array(repeating: 1, count: subviews.count))
default:
var fib: [Int128] = [1, 1]
for i in 2..<subviews.count {
fib.append(fib[i - 1] + fib[i - 2])
}
return Cache(fib: fib)
}
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
guard subviews.count - cache.fib.count > 0 else {
return
}
for i in 0..<(subviews.count - cache.fib.count) {
let c = cache.fib.count - 1
cache.fib.append(cache.fib[i + c] + cache.fib[i - 1 + c])
}
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
var cache = cache
var subviews = subviews
var bounds = bounds
var count = 0
while !subviews.isEmpty {
switch subviews.count {
case 1:
subviews[0].place(at: bounds.origin, proposal: ProposedViewSize(bounds.size))
default:
let i = subviews.count-1
if count % 2 == 0 {
var size = bounds.size
size.height *= (CGFloat(cache.fib[i]) / CGFloat(cache.fib[i] + cache.fib[i-1]))
if size.width <= size.height {
size.height = size.width
} else {
size.width = size.height
}
if count == 0 {
let h = size.height * (1 + CGFloat(cache.fib[i-1]) / CGFloat(cache.fib[i]))
bounds.origin.y += (bounds.size.height - h)/2
}
subviews.first?.place(at: bounds.origin, proposal: ProposedViewSize(size))
bounds.origin.y += size.height
bounds.size.height -= size.height
bounds.size.width = size.width
} else {
var size = bounds.size
size.width *= (CGFloat(cache.fib[i]) / CGFloat(cache.fib[i] + cache.fib[i-1]))
if size.height <= size.width {
size.width = size.height
} else {
size.height = size.width
}
subviews.first?.place(at: bounds.origin, proposal: ProposedViewSize(size))
bounds.origin.x += size.width
bounds.size.width -= size.width
bounds.size.height = size.height
}
}
cache.fib.removeLast()
subviews = subviews[1..<subviews.count]
count += 1
}
}
}
struct ContentView: View {
var numberOfItems = 3
var body: some View {
FibonacciLayout {
ForEach(0..<numberOfItems, id: \.self) { offset in
color(at: offset)
}
}
.padding()
}
func color(at index: Int) -> Color {
let hue = (Double(index) * .pi * (8.0 / 7.0)).truncatingRemainder(dividingBy: 1.0)
return Color(hue: hue, saturation: 0.6, brightness: 1.0)
}
}
#Preview {
@Previewable @State var numberOfItems: Int = 3
ContentView(numberOfItems: numberOfItems)
.frame(maxHeight: .infinity)
.overlay {
VStack {
Stepper("Count", value: $numberOfItems, in: 3...10)
Button("Reset") {
numberOfItems = 3
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(maxHeight: .infinity, alignment: .bottom)
.padding()
}
.animation(.default, value: numberOfItems)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment