Swift and SwiftUI tutorials for Swift Developers

Horizontal ScrollView in SwiftUI

In the vast universe of Swift programming, creating fluid, dynamic, and attractive user interfaces is a daily task for any iOS Developer. Gone are the days of dealing with delegates and complex configurations of UIScrollView in UIKit. Today, thanks to SwiftUI, creating scrollable views is more intuitive than ever.

In this comprehensive tutorial, we are going to break down everything you need to know about the Horizontal ScrollView in SwiftUI. We will explore from the most basic concepts to advanced optimization techniques and, most importantly, how to implement this master component across the Apple ecosystem using Swift and Xcode, covering iOS, macOS, and watchOS.


1. What is a Horizontal ScrollView in SwiftUI?

In its purest form, a ScrollView in SwiftUI is a container that allows the user to scroll through content that exceeds the size of the screen or the allocated viewing area. By default, a ScrollView is vertical, but by adjusting a simple parameter, we can transform it into a horizontal scrolling axis.

A Horizontal ScrollView in SwiftUI is essential for creating modern design patterns. Think about the applications you use every day:

  • Image carousels in e-commerce or social media apps.
  • Category or tag lists at the top of search screens.
  • Suggestion cards or user stories.
  • “Continue watching” sections on streaming platforms.

As an iOS Developer, mastering this component is not optional; it is an absolute necessity for creating top-tier user experiences (UX).

The Anatomy of a ScrollView

The base syntax in SwiftUI is incredibly clean. To create horizontal scrolling, we simply pass .horizontal to the ScrollView initializer, followed by the content, which is usually wrapped in an HStack (Horizontal Stack) to organize items from left to right.


2. Setting up our environment in Xcode

Before we write our first carousel, let’s make sure our environment is ready.

  1. Open Xcode.
  2. Create a new project by selecting File > New > Project.
  3. Choose the App template under the iOS, macOS, or watchOS tab (we’ll start with iOS, but the codebase we create will be cross-platform).
  4. Make sure the interface is set to SwiftUI and the language is Swift.
  5. Name your project (e.g., HorizontalScrollMastery).

We are now ready to start coding!


3. Basic Implementation: Your First Horizontal ScrollView

Let’s start with a simple example: a horizontal list of colored cards. This is the starting point for any Horizontal ScrollView in SwiftUI.

Open your ContentView.swift file and replace the code with the following:

import SwiftUI

struct ContentView: View {
    let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink, .yellow, .teal]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Featured Colors")
                .font(.title)
                .fontWeight(.bold)
                .padding(.horizontal)
            
            // Here is the magic: We initialize the ScrollView with the .horizontal axis
            ScrollView(.horizontal) {
                HStack(spacing: 15) {
                    ForEach(0..<colors.count, id: \.self) { index in
                        RoundedRectangle(cornerRadius: 15)
                            .fill(colors[index])
                            .frame(width: 150, height: 100)
                            .shadow(radius: 5)
                    }
                }
                .padding(.horizontal)
            }
        }
    }
}

#Preview {
    ContentView()
}

Code Analysis:

  1. ScrollView(.horizontal): Tells SwiftUI that the internal content should be scrollable from left to right (and vice versa).
  2. HStack(spacing: 15): Organizes our rounded rectangles into a row. The 15-point spacing provides breathing room between each card.
  3. ForEach: Iterates over our color array to generate the views dynamically. This is fundamental in modern Swift programming for handling data.

Hiding Scroll Indicators

Often, for aesthetic reasons, an iOS Developer prefers to hide the native horizontal scroll bar. SwiftUI makes this ridiculously easy using the showsIndicators parameter.

ScrollView(.horizontal, showsIndicators: false) {
    // Your HStack and content here...
}

With this simple change, the carousel looks much cleaner and more professional.


4. Extreme Performance: The Power of LazyHStack

If our colors array had only 10 elements, a normal HStack would work perfectly. But what if we are building a photo app and have 1,000 images in our Horizontal ScrollView in SwiftUI?

This is where many beginner developers make a critical mistake that impacts performance and memory. An HStack renders all of its child views immediately, even if they are not visible on the screen. For thousands of items, this would freeze the application.

The solution in Swift and SwiftUI is the LazyHStack.

What is LazyHStack?

As the name implies, a LazyHStack is “lazy”. It only renders views as they are about to appear on the screen and frees up the memory of those that have scrolled out of view.

Let’s refactor our code to simulate a large dataset and utilize best practices:

import SwiftUI

struct CardModel: Identifiable {
    let id = UUID()
    let title: String
    let color: Color
}

struct OptimizationView: View {
    // We generate 1000 items
    let cards = (1...1000).map { 
        CardModel(title: "Item \($0)", color: Color(hue: Double.random(in: 0...1), saturation: 0.8, brightness: 0.9)) 
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Optimized Scroll")
                .font(.largeTitle)
                .padding()
            
            ScrollView(.horizontal, showsIndicators: false) {
                // We replace HStack with LazyHStack
                LazyHStack(spacing: 20) {
                    ForEach(cards) { card in
                        CardView(card: card)
                    }
                }
                .padding(.horizontal)
            }
        }
    }
}

// Subview to keep the code clean
struct CardView: View {
    let card: CardModel
    
    var body: some View {
        VStack {
            Text(card.title)
                .font(.headline)
                .foregroundColor(.white)
        }
        .frame(width: 200, height: 250)
        .background(card.color)
        .cornerRadius(20)
        .shadow(color: card.color.opacity(0.5), radius: 10, x: 0, y: 5)
    }
}

#Preview {
    OptimizationView()
}

Using LazyHStack inside a Horizontal ScrollView in SwiftUI is an industry standard that every iOS Developer should apply when handling long lists of data.


5. Programmatic Control: ScrollViewReader

Sometimes, simply letting the user scroll manually isn’t enough. We might need to scroll the view programmatically; for example, jumping to the end of the list, returning to the beginning, or centering a specific item that the user selected elsewhere in the UI.

To achieve this in SwiftUI, we use ScrollViewReader. This structure provides us with a ScrollViewProxy, which exposes the scrollTo() function.

Let’s see how to implement it in Xcode, with proper animated scrolling:

import SwiftUI

struct ProgrammaticScrollView: View {
    let items = Array(1...50)
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                HStack {
                    Button("Go to start") {
                        withAnimation {
                            proxy.scrollTo(1, anchor: .leading)
                        }
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Spacer()
                    
                    Button("Go to end") {
                        withAnimation {
                            proxy.scrollTo(50, anchor: .trailing)
                        }
                    }
                    .buttonStyle(.borderedProminent)
                }
                .padding()
                
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHStack(spacing: 15) {
                        ForEach(items, id: \.self) { item in
                            Text("Section \(item)")
                                .font(.headline)
                                .frame(width: 120, height: 120)
                                .background(Color.indigo.gradient)
                                .foregroundColor(.white)
                                .cornerRadius(15)
                                .id(item) // <- The ID is crucial for the proxy to find it
                        }
                    }
                    .padding()
                }
            }
        }
    }
}

The .id() modifier is the bridge of communication. By calling proxy.scrollTo(50, anchor: .trailing), SwiftUI looks for the view with the ID 50 and scrolls the Horizontal ScrollView until that view aligns on the right edge (trailing).


6. Pagination and Snapping (New in iOS 17+)

For a long time, Swift developers had to resort to complex math in GeometryReader or fallback to UIKit (UICollectionView) to achieve a “pagination” effect (where the scroll stops exactly in the center of a card, creating a “page-like” carousel).

Fortunately, Apple introduced revolutionary native modifiers for SwiftUI in iOS 17 and macOS 14.

The key modifier is scrollTargetBehavior.

import SwiftUI

struct PaginationView: View {
    let colors: [Color] = [.red, .blue, .green, .orange, .purple]
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 0) { // 0 spacing for full-screen pagination
                ForEach(0..<colors.count, id: \.self) { index in
                    Rectangle()
                        .fill(colors[index].gradient)
                        // Width equal to the screen's width
                        .containerRelativeFrame(.horizontal)
                }
            }
        }
        // This modifier makes it snap to each view
        .scrollTargetBehavior(.paging) 
    }
}

ScrollTargetLayout for Spaced Carousels

If you don’t want full-screen pagination, but rather a card carousel where the active card centers itself (very popular in modern interface design), we use scrollTargetLayout() and scrollTargetBehavior(.viewAligned).

import SwiftUI

struct CenteredCarouselView: View {
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 20) {
                ForEach(1...10, id: \.self) { index in
                    RoundedRectangle(cornerRadius: 25)
                        .fill(Color.teal.gradient)
                        .frame(width: 300, height: 200)
                        .overlay(Text("Card \(index)").font(.title).bold().foregroundColor(.white))
                }
            }
            .padding(.horizontal, 40)
            // We indicate to SwiftUI that this layout is the target for snapping
            .scrollTargetLayout() 
        }
        // We tell the ScrollView to align the views when stopping
        .scrollTargetBehavior(.viewAligned)
    }
}

As an iOS Developer, adopting these new SwiftUI APIs drastically reduces your technical debt and improves the maintainability of your projects in Xcode.


7. Taking the Code to macOS and watchOS

One of the biggest promises of Swift programming and SwiftUI is “Learn once, apply anywhere”. And the truth is that our Horizontal ScrollView in SwiftUI is highly portable, but it requires slight platform-specific considerations.

Adapting for macOS

On macOS, the context changes from touch interactions to interactions with the mouse and trackpad.

The code we have written will work perfectly on macOS. However, you should keep in mind that on Mac, users often scroll horizontally by swiping with two fingers on the trackpad or holding down the Shift key while using the mouse wheel.

Design consideration on Mac: Windows on macOS can resize dynamically much more drastically than on iOS. Make sure to use flexible frames (.frame(maxWidth: .infinity)) or containerRelativeFrame so your cards don’t look tiny on a 4K monitor.

Adapting for watchOS

On watchOS, screen space is at a premium. A Horizontal ScrollView in SwiftUI is incredibly useful here for “card” style interfaces or workout pages.

On the Apple Watch, the user interacts by swiping their finger or using the Digital Crown. By default, the digital crown controls vertical scrolling. If your main view is a full-screen horizontal carousel, you might use a TabView instead of a ScrollView for pages, but if you need strict horizontal scrolling of smaller items, the code is the same.

// watchOS specific example
import SwiftUI

struct WatchCarouselView: View {
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 10) {
                ForEach(1...5, id: \.self) { item in
                    VStack {
                        Image(systemName: "heart.fill")
                            .font(.title)
                            .foregroundColor(.red)
                        Text("Pace \(item)")
                            .font(.caption2)
                    }
                    .frame(width: 80, height: 80)
                    .background(Color.gray.opacity(0.3))
                    .cornerRadius(40) // Circles for watchOS
                }
            }
            .padding(.horizontal)
        }
    }
}

8. Best Practices and Tips for a Pro iOS Developer

To close this guide, here are some golden rules of Swift programming that you should keep in mind when working with horizontal scrolling in Xcode:

  1. Accessibility (A11y) First: Ensure that the items inside your ScrollView have the correct accessibility modifiers. Use .accessibilityLabel() and .accessibilityHint() on your cards so VoiceOver users can understand what each item is about.
  2. Asynchronous Image Loading: If your carousel contains images hosted on the internet, never block the main thread. Use AsyncImage (introduced in iOS 15) inside your LazyHStack. This ensures that scrolling remains silky smooth at 60 or 120 FPS while images download in the background.
  3. Avoid Unnecessary Deep Nesting: Placing a horizontal ScrollView inside a vertical ScrollView is a common pattern (like in the App Store app), but avoid nesting multiple ScrollViews in the same direction, as it confuses user gestures and SwiftUI’s rendering engine.
  4. Haptic Feedback: If you implement custom snapping or reach the end of the list, consider adding slight haptic feedback using UIImpactFeedbackGenerator (on iOS) for a premium tactile feel.

Conclusion

The Horizontal ScrollView in SwiftUI is a formidable tool in your Swift programming arsenal. We have gone from simple static view declarations to massive memory optimization with LazyHStack, to absolute navigation control with ScrollViewReader, and finally, to the modern pagination techniques offered by the latest versions of SwiftUI.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

TimelineView in SwiftUI

Next Article

Create Button with System Image in SwiftUI

Related Posts