How to center a viewAligned ScrollView in SwiftUI

By default a ScrollView with  .scrollTargetBehavior(.viewAligned) is aligned to the leading edge. To get a .viewAligned ScrollView centered, you can use combination of .containerRelativeFrame, constant spacing, and optionally .contentMargins and .safeAreaPadding.

Center viewAligned ScrollView in iOS17 and iOS18 in SwiftUI in XCode
Center viewAligned ScrollView in iOS17 and iOS18 in SwiftUI in XCode

In SwiftUI, by default a ScrollView with  .scrollTargetBehavior(.viewAligned) is aligned to the leading edge.

To get a .viewAligned ScrollView centered, you can use combination of .containerRelativeFrame, constant spacing, and optionally .contentMargins and .safeAreaPadding.

Let's start with a basic code sample that demonstrates the default leading-edge alignment:

import SwiftUI

/// Card Model
struct Card: Identifiable {
    var id: Int
}

// Use the same spacing value with all modifiers
var hSpacing: CGFloat { 10.0 }

struct ContentView: View {
    // Create 30 Cards
    var cards: [Card] = (1..<30).map { Card(id: $0) }
    
    var body: some View {
        VStack {
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(alignment: .center, spacing: hSpacing) {
                    ForEach(cards) { card in
                        CardView(card: card)
                        .aspectRatio(16.0/9.0, contentMode: .fit)
                    }
                }
                .frame(height: 80)
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.viewAligned)
        }
    }
}

/// Single Card View
struct CardView: View {
    let card: Card
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .foregroundStyle(.blue.gradient)
            .overlay {
                Text(card.id, format: .number)
                    .foregroundStyle(.background)
                    .font(.largeTitle)
            }
    }
}

The resulting ScrollView looks like this:

Default leading-edge alignment when a LazyHStack is inside a .viewaligned ScrollView

To center the ScrollView, we can add the .containerRelativeFrame modifier:

.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: hSpacing)

The count and span parameters in .containerRelativeFrame can be confusing at first. You can use different combinations to show 1 and 1/2 cards, or any other fraction. This modifier is super useful!

ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(alignment: .center, spacing: hSpacing) {
                    ForEach(cards) { card in
                        CardView(card: card)
                            .aspectRatio(16.0/9.0, contentMode: .fit)
                            .containerRelativeFrame(
                                .horizontal, 
                                count: 3, 
                                span: 1, 
                                spacing: hSpacing
                            )
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.viewAligned)
Make sure to use the same spacing with all view modifiers. For this we created a constant hSpacing in the first code sample: var hSpacing: CGFloat { 10.0 }

The ScrollView now scrolls centered:

Centred viewAligned ScrollView in SwiftUI using the .conainerRelativeFrame modifier
Centred viewAligned ScrollView in SwiftUI using the .conainerRelativeFrame modifier

To have the cards peek into the ScrollView on the leading and trailing edges, we can use the .safeAreaPadding and .contentMargins modifiers. This gives a nice effect and indicates to the user that there is more content to scroll to:

ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(alignment: .center, spacing: hSpacing) {
                    ForEach(cards) { card in
                        CardView(card: card)
                            .aspectRatio(16.0/9.0, contentMode: .fit)
                            .containerRelativeFrame(
                            .horizontal, 
                            count: 3, 
                            span: 1, 
                            spacing: hSpacing
                            )
                    }
                }
                .scrollTargetLayout()
            }
            .safeAreaPadding(hSpacing)
            .contentMargins(hSpacing)
            .scrollTargetBehavior(.viewAligned)
LazyHStack inside a .viewAligned ScrollView using .containerRelativeFrame, .safeAreaPadding and .contentMargins
LazyHStack inside a .viewAligned ScrollView using .containerRelativeFrame, .safeAreaPadding and .contentMargins

You can change the count parameter in .containerRelativeFrame to another odd number to show more cards:

.containerRelativeFrame(.horizontal, count: 5, span: 1, spacing: hSpacing)
Centred ScrollView making use of viewAligned scrollTargetBehaviour, containerRelativeFrame, safeAreaPadding and contentMargins modifiers
Centred ScrollView making use of viewAligned scrollTargetBehaviour, containerRelativeFrame, safeAreaPadding and contentMargins modifiers