Here is a solution to your problem using the new Layout
protocol. I created a custom OverflowHStackLayout
that does all the work.
It displays all subviews as long as they fit in the given space.
The last subview in OverflowHStackLayout
has to be the overflow label (which can be fully customised).
The tricky part was to report the nr of overflow items back to the view, which is solved now by using a LayoutValueKey
.
If you want to dig into Layout yourself there is nothing better than: https://swiftui-lab.com/layout-protocol-part-1/
Have fun with it :)

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)
}
}
}
}