Skip to main content

ScrollView

Definition​

ScrollView is a scrollable container in SwiftUI that allows users to scroll through content that exceeds the available screen space. It supports both vertical and horizontal scrolling with various customization options.

Basic Syntax​

ScrollView {
// Content that can be scrolled
VStack {
ForEach(0..<50, id: \.self) { index in
Text("Item \(index)")
.padding()
}
}
}

Scroll Directions​

// Vertical scrolling (default)
ScrollView(.vertical) {
VStack {
ForEach(0..<20, id: \.self) { index in
Rectangle()
.fill(Color.blue.opacity(0.7))
.frame(height: 100)
.overlay(Text("Row \(index)"))
}
}
}

// Horizontal scrolling
ScrollView(.horizontal) {
HStack {
ForEach(0..<20, id: \.self) { index in
Rectangle()
.fill(Color.green.opacity(0.7))
.frame(width: 150, height: 100)
.overlay(Text("Col \(index)"))
}
}
}

// Both directions
ScrollView([.vertical, .horizontal]) {
LazyVStack {
ForEach(0..<10, id: \.self) { row in
LazyHStack {
ForEach(0..<10, id: \.self) { col in
Rectangle()
.fill(Color.purple.opacity(0.7))
.frame(width: 100, height: 100)
.overlay(Text("\(row),\(col)"))
}
}
}
}
}

Show/Hide Scroll Indicators​

// Hide scroll indicators
ScrollView {
LazyVStack {
ForEach(0..<100, id: \.self) { index in
Text("Hidden indicators \(index)")
.padding()
}
}
}
.scrollIndicators(.hidden)

// Show only vertical indicators
ScrollView([.vertical, .horizontal]) {
// Content
}
.scrollIndicators(.visible, axes: .vertical)

Scroll Position Control (iOS 17+)​

struct ScrollPositionExample: View {
@State private var scrollPosition: Int? = nil

var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(0..<100, id: \.self) { index in
Text("Item \(index)")
.font(.title2)
.frame(maxWidth: .infinity, minHeight: 50)
.background(Color.blue.opacity(0.1))
.id(index)
}
}
}
.scrollPosition(id: $scrollPosition)

HStack {
Button("Scroll to Top") {
withAnimation {
scrollPosition = 0
}
}
Button("Scroll to Middle") {
withAnimation {
scrollPosition = 50
}
}
Button("Scroll to Bottom") {
withAnimation {
scrollPosition = 99
}
}
}
.padding()
}
}
}

Lazy Loading​

// LazyVStack for vertical content
ScrollView {
LazyVStack(spacing: 10) {
ForEach(0..<1000, id: \.self) { index in
HStack {
AsyncImage(url: URL(string: "https://picsum.photos/60/60?random=\(index)")) { image in
image.resizable()
} placeholder: {
Color.gray
}
.frame(width: 60, height: 60)
.clipShape(Circle())

VStack(alignment: .leading) {
Text("Item \(index)")
.font(.headline)
Text("Description for item \(index)")
.font(.caption)
.foregroundColor(.secondary)
}

Spacer()
}
.padding(.horizontal)
.onAppear {
print("Item \(index) appeared")
}
}
}
}

// LazyHStack for horizontal content
ScrollView(.horizontal) {
LazyHStack(spacing: 10) {
ForEach(0..<100, id: \.self) { index in
VStack {
Circle()
.fill(Color.orange)
.frame(width: 80, height: 80)
.overlay(Text("\(index)"))
Text("Item \(index)")
.font(.caption)
}
.onAppear {
print("Horizontal item \(index) loaded")
}
}
}
.padding(.horizontal)
}

Custom Scroll Behavior​

struct CustomScrollView: View {
@State private var scrollOffset: CGFloat = 0

var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<50, id: \.self) { index in
Rectangle()
.fill(Color.blue.opacity(0.7))
.frame(height: 100)
.overlay(
Text("Item \(index)")
.foregroundColor(.white)
)
.scaleEffect(1.0 - abs(scrollOffset) * 0.001)
}
}
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).origin.y
)
}
)
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollOffset = value
}
}
}

struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

Paging ScrollView​

struct PagingScrollView: View {
let colors: [Color] = [.red, .blue, .green, .orange, .purple]

var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(0..<colors.count, id: \.self) { index in
Rectangle()
.fill(colors[index])
.frame(width: geometry.size.width, height: geometry.size.height)
.overlay(
Text("Page \(index + 1)")
.font(.largeTitle)
.foregroundColor(.white)
)
}
}
}
.scrollTargetBehavior(.paging) // iOS 17+
}
}
}

Pull to Refresh​

struct RefreshableScrollView: View {
@State private var items = Array(0..<20)
@State private var isLoading = false

var body: some View {
NavigationView {
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text("Item \(item)")
Spacer()
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.padding(.horizontal)
}
}
}
.refreshable {
await refreshData()
}
.navigationTitle("Refreshable List")
}
}

private func refreshData() async {
isLoading = true
// Simulate network call
try? await Task.sleep(nanoseconds: 2_000_000_000)
items = Array(0..<Int.random(in: 15...25))
isLoading = false
}
}

Nested ScrollViews​

struct NestedScrollExample: View {
var body: some View {
ScrollView {
VStack(spacing: 20) {
Text("Main Content")
.font(.title)
.padding()

// Horizontal scroll section
VStack(alignment: .leading) {
Text("Categories")
.font(.headline)
.padding(.horizontal)

ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
ForEach(["Food", "Travel", "Tech", "Sports", "Music"], id: \.self) { category in
Text(category)
.padding()
.background(Color.blue.opacity(0.2))
.cornerRadius(20)
}
}
.padding(.horizontal)
}
}

// More main content
ForEach(0..<10, id: \.self) { index in
VStack(alignment: .leading) {
Text("Section \(index)")
.font(.headline)

Text("Content for section \(index). This is some longer text that describes the content of this particular section.")
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal)
}
}
}
}
}

ScrollView with Sticky Headers​

struct StickyHeaderScrollView: View {
var body: some View {
ScrollView {
LazyVStack(pinnedViews: [.sectionHeaders]) {
ForEach(0..<5, id: \.self) { section in
Section {
ForEach(0..<10, id: \.self) { item in
HStack {
Text("Item \(item)")
Spacer()
Text("Section \(section)")
.foregroundColor(.secondary)
}
.padding()
.background(Color.white)
}
} header: {
Text("Section \(section)")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.9))
}
}
}
}
}
}

Performance Optimization​

struct OptimizedScrollView: View {
let items = Array(0..<10000)

var body: some View {
ScrollView {
// Use LazyVStack for better performance with large datasets
LazyVStack(spacing: 10) {
ForEach(items, id: \.self) { item in
OptimizedRow(item: item)
.onAppear {
// Load data when item appears
loadDataIfNeeded(for: item)
}
.onDisappear {
// Clean up resources when item disappears
cleanupIfNeeded(for: item)
}
}
}
}
}

private func loadDataIfNeeded(for item: Int) {
// Implement lazy loading logic
}

private func cleanupIfNeeded(for item: Int) {
// Implement cleanup logic
}
}

struct OptimizedRow: View {
let item: Int

var body: some View {
HStack {
Circle()
.fill(Color.blue)
.frame(width: 40, height: 40)
Text("Item \(item)")
Spacer()
}
.padding(.horizontal)
}
}

Custom Scroll Effects​

struct ParallaxScrollView: View {
var body: some View {
ScrollView {
VStack(spacing: 0) {
// Hero section with parallax effect
GeometryReader { geometry in
Image(systemName: "mountain.2.fill")
.font(.system(size: 100))
.foregroundColor(.green)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
LinearGradient(
gradient: Gradient(colors: [.blue, .green]),
startPoint: .top,
endPoint: .bottom
)
)
.offset(y: geometry.frame(in: .global).minY * 0.5)
}
.frame(height: 300)

// Content
LazyVStack {
ForEach(0..<50, id: \.self) { index in
Text("Content item \(index)")
.frame(maxWidth: .infinity, minHeight: 80)
.background(Color.white)
}
}
}
}
.ignoresSafeArea(edges: .top)
}
}

Best Practices​

  1. Use LazyVStack/LazyHStack: For large datasets to improve performance
  2. Avoid nested ScrollViews: Can cause scroll conflicts and poor UX
  3. Consider List: For simple data display, List might be more appropriate
  4. Handle safe areas: Use proper padding and safe area handling
  5. Test performance: Monitor performance with large datasets
  6. Use appropriate spacing: Maintain consistent spacing throughout
  7. Provide loading states: Show progress for async content loading

Common Use Cases​

  • Content feeds and timelines
  • Image galleries and carousels
  • Long form content and articles
  • Product catalogs and grids
  • Settings and form screens
  • Custom layouts requiring scrolling
  • Onboarding and tutorial flows