🦋
SwiftUI: 表示領域が不足したら要素を重ねて表示するHStack
SwiftUIのHStackで以下の図のようなことをしたい。
そんな時はLayout
を使う。
struct OverlappingHStack: Layout {
let alignment: HorizontalAlignment
let spacing: CGFloat
init(alignment: HorizontalAlignment = .center, spacing: CGFloat = 8) {
self.alignment = alignment
self.spacing = spacing
}
private func maxHeight(subviews: Subviews) -> CGFloat {
return subviews.map { $0.sizeThatFits(.unspecified).height }.max() ?? .zero
}
private func maxWidth(subviews: Subviews) -> CGFloat {
return subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? .zero
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
return CGSize(width: proposal.replacingUnspecifiedDimensions().width,
height: maxHeight(subviews: subviews))
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
if subviews.isEmpty { return }
let mh = maxHeight(subviews: subviews)
let mw = maxWidth(subviews: subviews)
let idealWidth = mw * CGFloat(subviews.count) + spacing * CGFloat(subviews.count - 1)
if bounds.width < idealWidth {
let w = (bounds.width - mw) / CGFloat(subviews.count - 1)
var point = CGPoint(x: bounds.minX + 0.5 * mw, y: bounds.midY)
subviews.indices.forEach { index in
subviews[index].place(at: point,
anchor: .center,
proposal: ProposedViewSize(width: mw, height: mh))
point.x += w
}
} else {
let diff = bounds.width - idealWidth
let offset: CGFloat
switch alignment {
case .leading: offset = 0
case .center: offset = 0.5 * diff
case .trailing: offset = diff
default: offset = 0.5 * diff
}
var point = CGPoint(x: bounds.minX + offset, y: bounds.midY)
subviews.indices.forEach { index in
subviews[index].place(at: point,
anchor: .leading,
proposal: ProposedViewSize(width: mw, height: mh))
point.x += mw
if index < subviews.count - 1 {
point.x += spacing
}
}
}
}
}
struct SampleView: View {
let array: [String] = ["⚽️", "🏀", "🏈", "⚾️", "🥎", "🎾", "🏐", "🏉", "🥏", "🎱"]
var body: some View {
VStack(alignment: .leading, spacing: 16) {
overlappingHStack(Array(array.prefix(2)))
overlappingHStack(Array(array.prefix(5)))
overlappingHStack(array)
}
.padding()
}
func overlappingHStack(_ array: [String]) -> some View {
OverlappingHStack(alignment: .trailing) {
ForEach(array, id: \.self) { item in
Text(item)
.font(.system(size: 100))
}
}
}
}
ちゃんと.leading
、.center
、.trailing
にも対応している。
左から.leading
、.center
、.trailing
重ねる順番を変更できるかは調査したい。
Discussion
この
HStack
のさらに発展index番目の要素が選択されている時は他が退くHStack