Last active
May 19, 2025 11:21
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Bash zphisher.sh