Skip to content

Instantly share code, notes, and snippets.

@mecid
Last active December 10, 2023 19:24
Show Gist options
  • Save mecid/e0d4d6652ccc8b5737449a01ee8cbc6f to your computer and use it in GitHub Desktop.
Save mecid/e0d4d6652ccc8b5737449a01ee8cbc6f to your computer and use it in GitHub Desktop.
PagerView in SwiftUI
//
// PagerView.swift
//
// Created by Majid Jabrayilov on 12/5/19.
// Copyright © 2019 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct PagerView<Content: View>: View {
let pageCount: Int
@Binding var currentIndex: Int
let content: Content
@GestureState private var translation: CGFloat = 0
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._currentIndex = currentIndex
self.content = content()
}
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentIndex) * geometry.size.width)
.offset(x: self.translation)
.animation(.interactiveSpring())
.gesture(
DragGesture().updating(self.$translation) { value, state, _ in
state = value.translation.width
}.onEnded { value in
let offset = value.translation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
}
)
}
}
}
@cecipirotto
Copy link

My team recently delivered PagerTabStripView, a new Open Source for Pager Tab views. This is the GitHub repo. You can read an introduction in this post.
Here is an example gif!
enter image description here

We'll appreciate it if you can take a look! If you like the library, want to contribute to the project, or need some help using it, please reach out to us, open an issue or a pull request.

@sinnuswong
Copy link

sinnuswong commented Apr 19, 2022

This will more smoothly

import Foundation
import SwiftUI

struct PagerView<Content: View>: View {
    let pageCount: Int
    @State var ignore: Bool = false
    @Binding var currentIndex: Int {
        didSet {
            if (!ignore) {
                currentFloatIndex = CGFloat(currentIndex)
            }
        }
    }
    @State var currentFloatIndex: CGFloat = 0 {
        didSet {
            ignore = true
            currentIndex = min(max(Int(currentFloatIndex.rounded()), 0), self.pageCount - 1)
            ignore = false
        }
    }
    let content: Content

    @GestureState private var offsetX: CGFloat = 0

    init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
        self.pageCount = pageCount
        self._currentIndex = currentIndex
        self.content = content()
    }

    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                self.content.frame(width: geometry.size.width)
            }
            .frame(width: geometry.size.width, alignment: .leading)
            .offset(x: -CGFloat(self.currentFloatIndex) * geometry.size.width)
            .offset(x: self.offsetX)
            .animation(.linear, value:offsetX)
            .highPriorityGesture(
                DragGesture().updating(self.$offsetX) { value, state, _ in
                    state = value.translation.width
                }
                .onEnded({ (value) in
                    let offset = value.translation.width / geometry.size.width
                    let offsetPredicted = value.predictedEndTranslation.width / geometry.size.width
                    let newIndex = CGFloat(self.currentFloatIndex) - offset
                    
                    self.currentFloatIndex = newIndex
                    
                    withAnimation(.easeOut) {
                        if(offsetPredicted < -0.5 && offset > -0.5) {
                            self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() + 1), 0), self.pageCount - 1))
                        } else if (offsetPredicted > 0.5 && offset < 0.5) {
                            self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() - 1), 0), self.pageCount - 1))
                        } else {
                            self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded()), 0), self.pageCount - 1))
                        }
                    }
                })
            )
        }
        .onChange(of: currentIndex, perform: { value in
            print("index changed")
            
            // this is probably animated twice, if the tab change occurs because of the drag gesture
            withAnimation(.easeOut) {
                currentFloatIndex = CGFloat(value)
            }
        })
    }
}

@samilao101
Copy link

Is there any way to load the views lazily?

@mecid
Copy link
Author

mecid commented May 5, 2022

@samilao101 did you try to use LazyHStack instead of HStack?

@samilao101
Copy link

@mecid I did, but the issue is that in order for a lazyhstack to become lazy it needs to be imbedded in a scrollview, and if you do that, you essentially lose the pagination feature.

@mecid
Copy link
Author

mecid commented May 5, 2022

@samilao101 oh, you're right. Maybe TabView with Page style will work for you better?

@pjcau
Copy link

pjcau commented May 30, 2022

So isn't the lazy this approach....Load all contents and onAppear

@samilao101
Copy link

@pjcau sorry I am not sure I understand what you mean

@pjcau
Copy link

pjcau commented May 30, 2022

@pjcau sorry I am not sure I understand what you mean

IN this way, if you want load a array of DetalView for example, so load in one step all init and onAppear(). So it's not good. But fix it with TabView with Page style.

Copy link

ghost commented Sep 27, 2022

@pjcau did you manage to integrate ScrollView in order to achieve lazy loading?

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