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​
- Use LazyVStack/LazyHStack: For large datasets to improve performance
- Avoid nested ScrollViews: Can cause scroll conflicts and poor UX
- Consider List: For simple data display, List might be more appropriate
- Handle safe areas: Use proper padding and safe area handling
- Test performance: Monitor performance with large datasets
- Use appropriate spacing: Maintain consistent spacing throughout
- 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