Skip to content

Instantly share code, notes, and snippets.

Created March 10, 2021 15:32
Show Gist options
  • Save wtpalexander/565ccf7bb092734dbf2c584dd639fb46 to your computer and use it in GitHub Desktop.
Save wtpalexander/565ccf7bb092734dbf2c584dd639fb46 to your computer and use it in GitHub Desktop.
Custom PagerView to replace PageTabViewStyle, that currently has performance issues with complex views. Originally created by @mecid (, I've changed the animation to work just like the PageTabViewStyle when changed using `withAnimation`.
// PagerView.swift
// Originally Created by Majid Jabrayilov on 12/5/19.
// Modified by Will Alexander on 10/3/2021
// Copyright © 2019 Majid Jabrayilov. All rights reserved.
// Copyright © 2020 appstrm. All rights reserved.
import SwiftUI
class PagerViewModel: ObservableObject {
@Published var lastDrag: CGFloat = 0.0
@Binding var currentIndex: Int
init(currentIndex: Binding<Int>) {
self._currentIndex = currentIndex
struct PagerView<Content: View>: View {
let pageCount: Int
@Binding var currentIndex: Int
let content: Content
@ObservedObject var viewModel: PagerViewModel
@GestureState private var translation: CGFloat = 0
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._currentIndex = currentIndex
self.content = content()
viewModel = PagerViewModel(currentIndex: currentIndex)
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
content.frame(width: geometry.size.width)
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: (-CGFloat(viewModel.currentIndex) * geometry.size.width) + viewModel.lastDrag)
DragGesture().updating($translation) { value, state, _ in
state = value.translation.width
viewModel.lastDrag = value.translation.width
}.onEnded { value in
let offset = value.predictedEndTranslation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
let newIndexBound = min(max(Int(newIndex), 0), self.pageCount - 1)
let indexToChangeTo = min(max(newIndexBound, currentIndex - 1), currentIndex + 1)
if indexToChangeTo != viewModel.currentIndex {
withAnimation(.easeOut) { viewModel.currentIndex = indexToChangeTo }
} else {
withAnimation(.easeOut) { viewModel.lastDrag = 0 }
struct PagerView_Previews: PreviewProvider {
static var previews: some View {
struct PagerViewWrapper: View {
@State var index = 0
var body: some View {
PagerView(pageCount: 4, currentIndex: $index) {
Copy link

Hi, thanks for the code ! I've tried to use it inside a scrollView but I'm experiencing two issues:
1-First, when a Text with a long string inside is one of the pager components, it is automatically truncated if it has no height specified
2-If there is a LazyGrid in the content: the height of the pager is well detected inside the scrollView (which was my main issue with TabView) but the scrollview itself doesn't wan't to scroll: when scrolling down, you see the bottom of the pager view, but it springs back as soon as you end the drag gesture. As if the scrollView wasn't aware of its inner size...
I'm really not that good at GeometryReader stuff so I can't really contribute here, but I was wandering if these issues can be fixed or if I need to switch to another solution (grids are mandatory for my project)...

I can send you the project if you're interested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment