Created
February 13, 2025 15:50
-
-
Save Codelaby/6241da1406ca12807cfbb57748d6cc47 to your computer and use it in GitHub Desktop.
InfiniteScrollView hack offset
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
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