Я хочу получить представление «чип», при котором первые чипы занимают максимальную ширину 2/3 экрана. Если фишек больше, должно отображаться количество, например +2.
Чтобы добиться этого, я думал обрезать HStack
. Однако мне нужно будет проверить размер чипа, прежде чем добавлять его в HStack
.
private func generateTags(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
[...]
return HStack {
ForEach(Array(tags.enumerated()), id: \.offset) { index, tag in
if width < g.size.width * 0.5 {
tagView(tag: tag)
.measureSize { size in
width += size.width
}
}
}
Spacer()
}
}
extension View {
func measureSize(perform action: @escaping (CGSize) -> Void) -> some View {
self.modifier(MeasureSizeModifier())
.onPreferenceChange(SizePreferenceKey.self, perform: action)
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct MeasureSizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self,
value: geometry.size)
})
}
}
На самом деле вы можете игнорировать значок карты и значок камеры. Чего я хочу добиться, так это чипов, которые используют полную ширину, но показывают усеченную информацию (например, +2), если есть больше чипов, которые не подходят.
Вот решение вашей проблемы с использованием нового протокола Layout
. Я создал кастом OverflowHStackLayout
, который делает всю работу.
Он отображает все подпредставления, если они помещаются в заданное пространство.
Последнее подпредставление в OverflowHStackLayout
должно быть меткой переполнения (которую можно полностью настроить).
Сложная часть заключалась в том, чтобы сообщить количество элементов переполнения обратно в представление, что теперь решается с помощью LayoutValueKey
.
Если вы хотите сами покопаться в Layout, то нет ничего лучше, чем: https://swiftui-lab.com/layout-protocol-part-1/
Получайте удовольствие от этого :)
struct Chip: Identifiable {
let id = UUID()
let icon: String
let name: String
}
let chips = [
Chip(icon: "😁", name: "Awesome"),
Chip(icon: "🌤️", name: "Weather"),
Chip(icon: "💼", name: "Work"),
Chip(icon: "🥳", name: "Party"),
Chip(icon: "⚽️", name: "Soccer"),
Chip(icon: "🍲", name: "Food"),
Chip(icon: "😁", name: "Awesome"),
Chip(icon: "🌤️", name: "Weather"),
Chip(icon: "💼", name: "Work"),
]
struct ContentView: View {
@State private var overflowCount = 0 // keeps track of the nr of overflow items
@State private var width = 500.0 // for testing only
var body: some View {
VStack {
GeometryReader { geo in
HStack {
// Custom Layout, last subview is the overflow label
OverflowHStackLayout {
ForEach(chips) { chip in
ChipView(chip: chip)
.padding(.leading, 5)
}
// Last view in this stack: Overflow label, using layoutvaluekey
Text("+ \(overflowCount)")
.font(.caption).bold()
.padding(.leading, 5)
.layoutValue(key: OverflowCounter.self, value: $overflowCount)
}
// ... the rest is standard
Spacer()
// pin And Camera View
Divider()
HStack {
ChipView(chip: Chip(icon: "📍", name: ""))
ChipView(chip: Chip(icon: "📷", name: ""))
}
.frame(width: geo.size.width / 3)
}
}
// for testing only
.border(.blue)
.frame(width: width)
Slider(value: $width, in: 100...1500)
}
.padding()
}
}
struct ChipView: View {
let chip: Chip
var body: some View {
HStack(spacing: 4) {
Text(chip.icon)
if chip.name.isEmpty == false {
Text(chip.name)
.lineLimit(1)
.font(.caption)
.bold()
}
}
.padding(6)
.background(
Capsule()
.fill(.gray).opacity(0.2)
)
}
}
// LayoutValueKey to report nr of overflow items back to view
struct OverflowCounter: LayoutValueKey {
static let defaultValue: Binding<Int>? = nil
}
// Custom Layout Struct, last subview is the overflow label
struct OverflowHStackLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
var totalHeight: CGFloat = 0
var totalWidth: CGFloat = subviews.last?.sizeThatFits(.unspecified).width ?? 0
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
for size in sizes.dropLast() {
if totalWidth + size.width <= (proposal.width ?? 0) {
totalWidth += size.width
totalHeight = max(totalHeight, size.height)
}
}
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var leadingX = bounds.minX
var runningWidth: CGFloat = subviews.last?.sizeThatFits(.unspecified).width ?? 0
var overflowItemsCount = 0
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
for index in subviews.indices.dropLast() {
if runningWidth + sizes[index].width <= (proposal.width ?? 0) {
subviews[index].place(
at: CGPoint(x: leadingX, y: bounds.midY),
anchor: .leading,
proposal: ProposedViewSize(sizes[index])
)
runningWidth += sizes[index].width
leadingX += sizes[index].width
} else {
overflowItemsCount += 1
// place overflowing items out of screen
subviews[index].place(at: CGPoint(x: -10000, y: -10000), proposal: .unspecified)
}
}
if let last = subviews.indices.last {
// if view has overflown, place last subview which is the overflow label
if overflowItemsCount > 0 {
subviews[last].place(
at: CGPoint(x: leadingX, y: bounds.midY),
anchor: .leading,
proposal: ProposedViewSize(sizes[last])
)
DispatchQueue.main.async {
subviews[last][OverflowCounter.self]?.wrappedValue = overflowItemsCount
}
} else {
// place overflow label out of screen
subviews[last].place(at: CGPoint(x: -10000, y: -10000), proposal: .unspecified)
}
}
}
}