Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Created February 13, 2025 15:50
Show Gist options
  • Save Codelaby/6241da1406ca12807cfbb57748d6cc47 to your computer and use it in GitHub Desktop.
Save Codelaby/6241da1406ca12807cfbb57748d6cc47 to your computer and use it in GitHub Desktop.
InfiniteScrollView hack offset
import SwiftUI
// MARK: InfiniteScrollView
struct InfiniteScrollView<Content: View>: View {
@State private var scrollPosition: ScrollPosition = ScrollPosition(idType: Int.self)
@Binding var currentIndex: Int
var spacing: CGFloat = 10
var itemSize: CGSize
var count: Int = 0
private let bufferSize = 1000
@ViewBuilder var content: Content
/// View Properties
//@State private var contentSize: CGSize = .zero
@State private var isScrolling: Bool = false
@State private var offsetX: CGFloat = .zero
var body: some View {
let sourceWidth = (itemSize.width + spacing) * CGFloat(bufferSize)
let relativeWidth = (itemSize.width + spacing) * CGFloat(count)
//let _ = print("sourceWidth", sourceWidth, "relativeWidth", relativeWidth)
Group (subviews: content) { collection in
// Calcular el número de repeticiones necesarias
let repetitions = Int(round(Double(bufferSize) / Double(count)))
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: spacing) {
//HStack(spacing: spacing) {
ForEach(0..<repetitions, id: \.self) { index in
ForEach(Array(collection.enumerated()), id: \.element.id) { index, subview in
subview
.id(index)
}
}
}
.scrollTargetLayout()
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .scrollView).origin.x
} action: { value in
self.offsetX = abs(value)
}
}
.scrollPosition($scrollPosition, anchor: .center)
.scrollClipDisabled()
.onAppear {
let adjustedOffsetX = adjustedOffsetX(offsetX,sourceWidth: sourceWidth, relativeWidth: relativeWidth) - itemSize.width / 2
scrollPosition.scrollTo(x: adjustedOffsetX)
}
.onChange(of: scrollPosition, initial: true, { oldValue, newValue in
if let currentIndex = newValue.viewID(type: Int.self) {
//withAnimation(.smooth) {
DispatchQueue.main.asyncAfter(deadline: .now() ) {
self.currentIndex = currentIndex
}
//}
}
})
.onScrollPhaseChange({ _, newPhase in
if newPhase == .idle {
let adjustedOffsetX = adjustedOffsetX(offsetX,sourceWidth: sourceWidth, relativeWidth: relativeWidth)
print("scrollOffsetX", offsetX, "fixedOffset", adjustedOffsetX)
scrollPosition.scrollTo(x: adjustedOffsetX)
}
})
}
}
func adjustedOffsetX(_ offsetX: CGFloat, sourceWidth: CGFloat, relativeWidth: CGFloat) -> CGFloat {
let center = sourceWidth / 2
let adjusted = ((offsetX - center).truncatingRemainder(dividingBy: relativeWidth)) + center
return adjusted
}
}
// MARK: Model
fileprivate struct CardModel: Identifiable, Hashable {
let id: UUID = .init()
let image: String
static let allItems: [CardModel] = {
return (1...5).map { CardModel(image: "apple_invites_\($0)") }
}()
}
// MARK: PlayGround
struct AppleInviteCarouselDemo: View {
//@State private var scrollPosition = ScrollPosition(idType: Int.self)
@State private var currentIndex: Int = 0
@State private var currentCard: CardModel?
var body: some View {
//let currentCard = CardModel.allItems[scrollPosition.viewID(id: Int.self)]
ZStack {
AmbientBackground(for: currentCard)
InfiniteScrollView(currentIndex: $currentIndex, spacing: 10, itemSize: CGSize(width: 220, height: 330), count: CardModel.allItems.count) {
ForEach(CardModel.allItems, id: \.self) { item in
CarouselCardView(from: item)
.frame(width: 220, height: 330)
}
}
.frame(height: 340)
//.border(.red)
.onChange(of: currentIndex) { oldValue, newValue in
//print("currentIndex", currentIndex)
// if let currentIndex = newValue.viewID(type: Int.self) {
// print(currentIndex)
if let currentItem = getItem(at: currentIndex) {
currentCard = currentItem
}
// }
}
}
}
private func getItem(at index: Int) -> CardModel? {
guard index >= 0 && index < CardModel.allItems.count else { return nil }
return CardModel.allItems[index]
}
@ViewBuilder
private func CarouselCardView(from item: CardModel) -> some View {
Rectangle()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
Image(item.image)
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
}
.clipShape(RoundedRectangle(cornerRadius: 20))
//.shadow(color: .black.opacity(0.4), radius: 10, x: 1, y: 0)
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
content
.offset(y: phase.isIdentity ? -10 : 0)
.rotationEffect(.degrees(phase.value * 3), anchor: .bottom)
}
.offset(y: 5)
}
@ViewBuilder
private func AmbientBackground(for item: CardModel?) -> some View {
ZStack {
ForEach(CardModel.allItems) { card in
Image(card.image )
.resizable()
.aspectRatio(contentMode: .fill)
.opacity(card == item ? 1 : 0)
//.animation(.smooth, value: item?.image)
}
// Overlay semi-transperent
Rectangle()
.fill(.black.opacity(0.45))
}
.compositingGroup()
.blur(radius: 90, opaque: true)
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.animation(.smooth, value: item)
}
}
#Preview {
//
// SampleTitleView(title: "Apple Invites carousel with SwiftUI", summary: "Auto size + Customization + RTL Support")
// Spacer()
AppleInviteCarouselDemo()
//.environment(\.layoutDirection, .rightToLeft)
// Spacer()
// CreditsView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment