#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: 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, 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, 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 } } } } }