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.
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:
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 constanthSpacing
in the first code sample:var hSpacing: CGFloat { 10.0 }
The ScrollView
now scrolls centered:
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)
You can change the count
parameter in .containerRelativeFrame
to another odd number to show more cards:
.containerRelativeFrame(.horizontal, count: 5, span: 1, spacing: hSpacing)