Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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)
}
)
}
}
}
@Plnda

This comment has been minimized.

Copy link

@Plnda Plnda commented Jan 20, 2020

@mecid How would you solve animation on the pages? now when I have a animation set to a page the whole thing starts animating when I drag

@mecid

This comment has been minimized.

Copy link
Owner Author

@mecid mecid commented Jan 20, 2020

@mecid How would you solve animation on the pages? now when I have a animation set to a page the whole thing starts animating when I drag

I'm not sure I understand the problem, could you please describe it?

@Plnda

This comment has been minimized.

Copy link

@Plnda Plnda commented Jan 20, 2020

@mecid

This comment has been minimized.

Copy link
Owner Author

@mecid mecid commented Jan 20, 2020

@majid Here is a sample + relevant code https://gist.github.com/Plnda/121b5960807074cf5fd68c68f959aa54

Interesting issue, you can add animation(nil) modifier to the content inside PagerView, I think it should help.

@Rep0se

This comment has been minimized.

Copy link

@Rep0se Rep0se commented Jan 22, 2020

@mecid A bug I noticed (which I've seen in some other apps) when you are switching between vertical scroll views diagonally, all objects will shift horizontally and remain that way until you scroll horizontally again to the next page. Is there a way to disable horizontal scrolling while you are scrolling vertically?
Also, what would change scroll sensitivity?
Edit: found another bug, where an image does not update until the end of the transition.

@fnazarios

This comment has been minimized.

Copy link

@fnazarios fnazarios commented May 29, 2020

I've added a Page Control at bottom.

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
            ZStack {
                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)
                    }
                )
                
                VStack {
                    Spacer()
                    
                    HStack {
                        ForEach(0..<self.pageCount, id: \.self) { index in
                            Circle()
                                .fill(index == self.currentIndex ? Color.white : Color.gray)
                                .frame(width: 10, height: 10)
                        }
                    }
                }
                .offset(y: 16)
            }
        }
    }
}
@mecid

This comment has been minimized.

Copy link
Owner Author

@mecid mecid commented Jun 3, 2020

@fnazarios that is great! Thanks 🙏🏻

@mmosimann

This comment has been minimized.

Copy link

@mmosimann mmosimann commented Jul 20, 2020

@majid Here is a sample + relevant code https://gist.github.com/Plnda/121b5960807074cf5fd68c68f959aa54

Interesting issue, you can add animation(nil) modifier to the content inside PagerView, I think it should help.

Did you guys found a solution for this problem? I still facing the problem. :(

@mecid

This comment has been minimized.

Copy link
Owner Author

@mecid mecid commented Jul 20, 2020

@mmosimann disabling animation with .animation(nil) or applying it by withAnimation should solve the issue.

@d03090

This comment has been minimized.

Copy link

@d03090 d03090 commented Dec 13, 2020

Hello! I have tried to make the animation a bit smoother and also tried to use predictedEndTranslation for faster swiping through. I had to change the currentIndex from Int to Float for it to work. Now I'm struggling to get the Binding back to Int, because an Int Index would definitely make more sense.
I would be glad, if you would tell me what you think about the changes and do you know how I can change the Binding back to an Int? Is this possible with Custom Bindings or something like that?

import SwiftUI

struct PagerView<Content: View>: View {
    let pageCount: Int
    @Binding var currentIndex: Int
    @State var currentFloatIndex: CGFloat = 0
    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)
            .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))
                        }
                    }
                })
            )
        }
    }
}
@Degtyarserg

This comment has been minimized.

Copy link

@Degtyarserg Degtyarserg commented Dec 23, 2020

@d03090 The animation looks very nice. Have you found a solution for Binding?

@d03090

This comment has been minimized.

Copy link

@d03090 d03090 commented Dec 23, 2020

@Degtyarserg

Thanks. I just have a VERY UGLY solution and I would be very happy, if someone can do it better.

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)
            .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)
            }
        })
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.