Pager / Swiper View in SwiftUI
Reading time: 3 min
This recipe shows how to implement a swipeable pager view in SwiftUI.
The end result will look like this:
A pager view lays its child views out as pages - single, currently selected child view occupies the entire pager. The user can swipe left or right to move through different pages.
First you'll implement the core PagerView
, and then afterwards you'll expand it to include bottom dots.
PagerView implementation
The PagerView
maps a sequence of Hashable
data to Views
, while keeping track of the index of the currently displayed child view.
Here's the code:
struct PagerView<Data, Content>: View
where Data : RandomAccessCollection, Data.Element : Hashable, Content : View {
// the source data to render, can be a range, an array, or any other collection of Hashable
private let data: Data
// the index currently displayed page
@Binding var currentIndex: Int
// maps data to page views
private let content: (Data.Element) -> Content
// keeps track of how much did user swipe left or right
@GestureState private var translation: CGFloat = 0
// the custom init is here to allow for @ViewBuilder for
// defining content mapping
init(_ data: Data,
currentIndex: Binding<Int>,
@ViewBuilder content: @escaping (Data.Element) -> Content) {
self.data = data
_currentIndex = currentIndex
self.content = content
}
var body: some View {
GeometryReader { geometry in
LazyHStack(spacing: 0) {
// render all the content, making sure that each page fills
// the entire PagerView
ForEach(data, id: \.self) { elem in
content(elem)
.frame(width: geometry.size.width)
}
}
.frame(width: geometry.size.width, alignment: .leading)
// the first offset determines which page is shown
.offset(x: -CGFloat(currentIndex) * geometry.size.width)
// the second offset translates the page based on swipe
.offset(x: translation)
.animation(.interactiveSpring())
.gesture(
DragGesture().updating($translation) { value, state, _ in
state = value.translation.width
}.onEnded { value in
// determine how much was the page swiped to decide if the current page
// should change (and if it's going to be to the left or right)
// 1.25 is the parameter that defines how much does the user need to swipe
// for the page to change. 1.0 would require swiping all the way to the edge
// of the screen to change the page.
let offset = value.translation.width / geometry.size.width * 1.25
let newIndex = (CGFloat(currentIndex) - offset).rounded()
currentIndex = min(max(Int(newIndex), 0), data.count - 1)
}
)
}
}
}
While the example above uses LazyHStack
for performance reasons, using a simple HStack
is also fine (and necessary if you're targetting iOS 13).
BONUS: PagerView with dots
Here's an implementation of a PagerView
with dots at the bottom that indicate which page is currently selected:
struct PagerViewWithDots<Data, Content>: View
where Data : RandomAccessCollection, Data.Element : Hashable, Content : View {
@State private var currentIndex = 0
private let data: Data
private let content: (Data.Element) -> Content
init(_ data: Data,
@ViewBuilder content: @escaping (Data.Element) -> Content) {
self.data = data
self.content = content
}
var body: some View {
ZStack {
// let the PagerView and the dots fill the available screen
Rectangle().foregroundColor(.white)
// render the Pager View
PagerView(data, currentIndex: $currentIndex, content: content)
// the dots view
VStack {
Spacer() // align the dots at the bottom
HStack(spacing: 6) {
ForEach(0..<data.count) { index in
Circle()
.foregroundColor((index == currentIndex) ? .black : .gray)
.frame(width: 10, height: 10)
}
}
}.padding()
}
}
}