gh-Dimillian-Skills/swiftui-ui-patterns/references/scrollview.md
Thomas Ricouard 70a15d08db Add SwiftUI UI patterns skill and references
Introduces the 'swiftui-ui-patterns' skill to docs/skills.json, providing best practices and example-driven guidance for building SwiftUI views and components. Adds SKILL.md and a comprehensive set of reference files covering TabView, NavigationStack, sheets, forms, controls, grids, overlays, haptics, focus handling, media, matched transitions, split views, and more.
2026-01-04 18:26:56 +01:00

2.3 KiB
Raw Permalink Blame History

ScrollView and Lazy stacks

Intent

Use ScrollView with LazyVStack, LazyHStack, or LazyVGrid when you need custom layout, mixed content, or horizontal/ grid-based scrolling.

Core patterns

  • Prefer ScrollView + LazyVStack for chat-like or custom feed layouts.
  • Use ScrollView(.horizontal) + LazyHStack for chips, tags, avatars, and media strips.
  • Use LazyVGrid for icon/media grids; prefer adaptive columns when possible.
  • Use ScrollViewReader for scroll-to-top/bottom and anchor-based jumps.
  • Use safeAreaInset(edge:) for input bars that should stick above the keyboard.

Example: vertical custom feed

@MainActor
struct ConversationView: View {
  private enum Constants { static let bottomAnchor = "bottom" }
  @State private var scrollProxy: ScrollViewProxy?

  var body: some View {
    ScrollViewReader { proxy in
      ScrollView {
        LazyVStack {
          ForEach(messages) { message in
            MessageRow(message: message)
              .id(message.id)
          }
          Color.clear.frame(height: 1).id(Constants.bottomAnchor)
        }
        .padding(.horizontal, .layoutPadding)
      }
      .safeAreaInset(edge: .bottom) {
        MessageInputBar()
      }
      .onAppear {
        scrollProxy = proxy
        withAnimation {
          proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
        }
      }
    }
  }
}

Example: horizontal chips

ScrollView(.horizontal, showsIndicators: false) {
  LazyHStack(spacing: 8) {
    ForEach(chips) { chip in
      ChipView(chip: chip)
    }
  }
}

Example: adaptive grid

let columns = [GridItem(.adaptive(minimum: 120))]

ScrollView {
  LazyVGrid(columns: columns, spacing: 8) {
    ForEach(items) { item in
      GridItemView(item: item)
    }
  }
  .padding(8)
}

Design choices to keep

  • Use Lazy* stacks when item counts are large or unknown.
  • Use non-lazy stacks for small, fixed-size content to avoid lazy overhead.
  • Keep IDs stable when using ScrollViewReader.
  • Prefer explicit animations (withAnimation) when scrolling to an ID.

Pitfalls

  • Avoid nesting scroll views of the same axis; it causes gesture conflicts.
  • Dont combine List and ScrollView in the same hierarchy without a clear reason.
  • Overuse of LazyVStack for tiny content can add unnecessary complexity.