List earns its keep on huge uniform datasets, mailboxes and to-do lists. For a screen of
mixed cards, ScrollView plus a lazy stack gives full control of the look, and the container
view APIs rebuild List's conveniences as small reusable primitives.
ScrollingSurface: the screen root
A thin wrapper picking LazyVStack or LazyHStack by direction, with scrollTargetLayout
applied. Every screen uses it as the root.
public struct ScrollingSurface<Content: View>: View {
public enum Direction {
case vertical(HorizontalAlignment)
case horizontal(VerticalAlignment)
}
let direction: Direction
let spacing: CGFloat?
let content: Content
public var body: some View {
switch direction {
case .vertical(let alignment):
ScrollView(.vertical) {
LazyVStack(alignment: alignment, spacing: spacing) { content }
.scrollTargetLayout()
.padding()
}
case .horizontal(let alignment):
ScrollView(.horizontal) {
LazyHStack(alignment: alignment, spacing: spacing) { content }
.scrollTargetLayout()
.padding()
}
}
}
}
DividedCard: grouped rows via Group(subviews:)
Group(subviews:) decomposes the caller's children, so the card can put a Divider between
rows, skipping the last, then wrap everything in a rounded material background.
public struct DividedCard<Content: View>: View {
let content: Content
public var body: some View {
Group(subviews: content) { subviews in
if !subviews.isEmpty {
VStack(alignment: .leading) {
ForEach(subviews) { subview in
subview
if subviews.last?.id != subview.id {
Divider().padding(.vertical, 8)
}
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 32))
}
}
}
}
SectionedSurface and the navigation chevron
ForEach(sections:) renders each Section's header, content, and footer, skipping empty
sections. The one List behavior worth recreating by hand is the NavigationLink chevron: a
custom ButtonStyle adds it, with contentShape(.rect) keeping the whole row tappable.
public struct NavigationButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label.opacity(configuration.isPressed ? 0.7 : 1)
Spacer()
Image(systemName: "chevron.right").foregroundStyle(.tertiary)
}
.contentShape(.rect)
}
}
Compose them like the List API: ScrollingSurface { SectionedSurface { ...sections... } } with
.buttonStyle(.navigation) on the screen. A screen without sections just drops
SectionedSurface. Remember that listRowBackground, listItemTint, and listRowInsets do
nothing outside a List, which is why these primitives exist.