public protocol Layout : Animatable {
associatedtype Cache = Void
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGSize
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
)
//...
}
public func replacingUnspecifiedDimensions(
by size: CGSize = CGSize(width: 10, height: 10)
) -> CGSize
struct CircleLayout: Layout {
let radius: CGFloat
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
let maxSize = getMaxSize(for: subviews)
return CGSize(
width: radius * 2 + maxSize.width,
height: radius * 2 + maxSize.height
)
}
//...
private func getMaxSize(for subviews: Subviews) -> CGSize {
subviews.reduce(.zero) { currentMaxSize, subview in
let currentSize = subview.sizeThatFits(.unspecified)
return CGSize(
width: max(currentSize.width, currentMaxSize.width),
height: max(currentSize.height, currentMaxSize.height)
)
}
}
}
struct CircleLayout: Layout {
//...
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let maxSize = getMaxSize(for: subviews)
let proposal = ProposedViewSize(maxSize)
for (index, subview) in subviews.enumerated() {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let origin = getPosition(
forSubviewIndex: index,
radius: radius,
center: center,
subviewsCount: subviews.count
)
subview.place(at: origin, proposal: proposal)
}
}
//...
private func getPosition(
forSubviewIndex index: Int,
radius: CGFloat,
center: CGPoint,
subviewsCount: Int
) -> CGPoint {
let theta = (CGFloat.pi * 2 / CGFloat(subviewsCount)) * CGFloat(index)
return CGPoint(
x: center.x + radius * cos(theta),
y: center.y + radius * sin(theta)
)
}
}
public protocol Layout : Animatable {
static var layoutProperties: LayoutProperties { get }
associatedtype Cache = Void
typealias Subviews = LayoutSubviews
func makeCache(subviews: Self.Subviews) -> Self.Cache
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing
func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGSize
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache)
func explicitAlignment(of guide: HorizontalAlignment, in bounds: CGRect, proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGFloat?
func explicitAlignment(of guide: VerticalAlignment, in bounds: CGRect, proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGFloat?
}
enum LayoutKind: Int, CaseIterable {
case vertical
case horizontal
case z
case grid
case circle
}
@State var layoutKind: LayoutKind = .vertical
// Между бетами стандартные классы Layout-ов могут меняться
// В будущем, в идеале можно будет использовать стандартные VStack и HStack
var layout: AnyLayout {
switch layoutKind {
case .vertical:
return AnyLayout(_VStackLayout())
case .horizontal:
return AnyLayout(_HStackLayout())
case .z:
return AnyLayout(_ZStackLayout())
case .grid:
return AnyLayout(_GridLayout())
case .circle:
return AnyLayout(CircleLayout(radius: 100))
}
}