Skip to content

Instantly share code, notes, and snippets.

@EngOmarElsayed
Last active May 19, 2025 11:21
Show Gist options
  • Save EngOmarElsayed/49506d6b43b74ea5829ffc40680dd055 to your computer and use it in GitHub Desktop.
Save EngOmarElsayed/49506d6b43b74ea5829ffc40680dd055 to your computer and use it in GitHub Desktop.
A custom Container I created to easily create Paging scroll view in swiftUI, article: https://thinkdiffrent.substack.com/p/creating-paging-scrollview-using
#Preview {
@Previewable @State var currentPage = 0
HPagingScrollView(currentPage: $currentPage, spacing: 30, pageWidth: 200, pageHeight: 450) {
RoundedRectangle(cornerRadius: 20)
RoundedRectangle(cornerRadius: 20)
RoundedRectangle(cornerRadius: 20)
RoundedRectangle(cornerRadius: 20)
}
.isScrollDisabled(false)
.environment(\.layoutDirection, .rightToLeft)
}
// Mark: HPagingScrollView
struct HPagingScrollView<Content: View>: View {
@Binding var currentPage: Int
let spacing: CGFloat
let pageWidth: CGFloat
let pageHeight: CGFloat
@ViewBuilder let content: () -> Content
private let isScrollDisabled: Bool
init(
currentPage: Binding<Int>,
spacing: CGFloat,
pageWidth: CGFloat,
pageHeight: CGFloat,
@ViewBuilder content: @escaping () -> Content
) {
_currentPage = currentPage
self.spacing = spacing
self.pageWidth = pageWidth
self.pageHeight = pageHeight
self.content = content
self.isScrollDisabled = false
}
var body: some View {
_VariadicView.Tree(
_HPagingScrollViewRoot(
currentPage: $currentPage,
spacing: spacing,
pageWidth: pageWidth,
pageHeight: pageHeight,
isScrollDisabled: isScrollDisabled
)
) {
content()
}
}
}
//MARK: - Disable scroll modifier
extension HPagingScrollView {
private init(
currentPage: Binding<Int>,
spacing: CGFloat,
pageWidth: CGFloat,
pageHeight: CGFloat,
isScrollDisabled: Bool,
@ViewBuilder content: @escaping () -> Content
) {
_currentPage = currentPage
self.spacing = spacing
self.pageWidth = pageWidth
self.pageHeight = pageHeight
self.content = content
self.isScrollDisabled = isScrollDisabled
}
public func isScrollDisabled(_ isScrollDisabled: Bool) -> HPagingScrollView {
HPagingScrollView(
currentPage: $currentPage,
spacing: spacing,
pageWidth: pageWidth,
pageHeight: pageHeight,
isScrollDisabled: isScrollDisabled,
content: content
)
}
}
struct _HPagingScrollViewRoot: _VariadicView_MultiViewRoot {
@Binding var currentPage: Int
let spacing: CGFloat
let pageWidth: CGFloat
let pageHeight: CGFloat
let isScrollDisabled: Bool
@Environment(\.layoutDirection) private var layoutDirection
@State private var offset: CGFloat = 0
private let screenWidth = UIScreen.main.bounds.width
private let screenHeight = UIScreen.main.bounds.height
func body(children: _VariadicView.Children) -> some View {
HStack(spacing: spacing) {
ForEach(children) { child in
child
.frame(width: pageWidth, height: pageHeight)
.id(child.id)
}
}
.padding(.horizontal, (screenWidth-pageWidth)/2)
.frame(width: screenWidth, alignment: .leading)
.offset(x: offset)
.gesture(scrollGesture(totalPages: children.count))
.onAppear {
offset = CGFloat(currentPage) * -(pageWidth+spacing)
}
}
private func scrollGesture(totalPages: Int) -> some Gesture {
DragGesture()
.onChanged { value in bounceAnimation(value, for: totalPages) }
.onEnded { value in
if layoutDirection == .leftToRight {
let isSwipeIncrementDirection = (value.translation.width < -(pageWidth+spacing/2) || value.velocity.width < -50)
let isSwipeDecrementDirection = (value.translation.width > (pageWidth+spacing/2) || value.velocity.width > 50)
if isSwipeIncrementDirection && isScrollDisabled == false { incrementCurrentPage(totalPages: totalPages-1) } else {
withAnimation {
offset = CGFloat(currentPage) * -(pageWidth+spacing)
}
}
if isSwipeDecrementDirection && isScrollDisabled == false { decrementCurrentPage() } else {
withAnimation {
offset = CGFloat(currentPage) * -(pageWidth+spacing)
}
}
} else {
let isSwipeIncrementDirection = (value.translation.width > (pageWidth+spacing/2) || value.velocity.width > 50)
let isSwipeDecrementDirection = (value.translation.width < -(pageWidth+spacing/2) || value.velocity.width < -50)
if isSwipeDecrementDirection && isScrollDisabled == false { decrementCurrentPage() }
if isSwipeIncrementDirection && isScrollDisabled == false { incrementCurrentPage(totalPages: totalPages-1) }
}
}
}
private func incrementCurrentPage(totalPages: Int) {
withAnimation {
currentPage = currentPage == totalPages ? currentPage: currentPage+1
offset = CGFloat(currentPage) * -(pageWidth+spacing)
}
}
private func decrementCurrentPage() {
withAnimation {
currentPage = currentPage == 0 ? 0: currentPage-1
offset = CGFloat(currentPage) * -(pageWidth+spacing)
}
}
private func bounceAnimation(_ value: DragGesture.Value, for totalPages: Int) {
let maxOffset = (CGFloat(totalPages-1) * -(pageWidth+spacing))-(300)
let minOffset = (CGFloat(0) * -(pageWidth+spacing))+(150)
let isLeftDirection: Bool = value.translation.width < -50
let isRightDirection: Bool = value.translation.width > 50
if layoutDirection == .leftToRight {
withAnimation(.spring(dampingFraction: 0.3)) {
if isLeftDirection, currentPage == totalPages-1, offset > maxOffset {
offset -= 0.009 * value.location.x
} else if currentPage == 0, offset < minOffset, isRightDirection {
offset += 0.009 * value.location.x
}
}
} else {
withAnimation(.spring(dampingFraction: 0.3)) {
if isLeftDirection, currentPage == 0, offset < minOffset {
offset += 0.009 * value.location.x
} else if currentPage == totalPages-1, offset > maxOffset, isRightDirection {
offset -= 0.009 * value.location.x
}
}
}
}
}
@Sowsow123
Copy link

Bash zphisher.sh

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