Skip to content

Minny27/InfiniteCarouselView

Repository files navigation

InfiniteCarouselView

A SwiftUI infinite paging carousel built with the tripling strategy. Supports tap-to-page, auto-scroll, and swipe-with-spring in a single generic view.

InfiniteCarouselView Demo


Overview

InfiniteCarouselView solves the main interaction problems with infinite carousels in SwiftUI:

1. Seamless looping without animation glitches Items are triplicated internally — [clone_front | real | clone_back]. When the scroll settles in a clone region, the view silently jumps back to the matching position in the real region. Because the content is identical, the user never notices.

displayIndex:  0  1  2  3  4 │  5  6  7  8  9 │ 10 11 12 13 14
               [ clone_front ] [     real      ] [  clone_back  ]
selectedIndex: 0  1  2  3  4    0  1  2  3  4    0  1  2  3  4

2. Snappy swipe — no deceleration Standard ScrollTargetBehavior hands animation control to UIKit, which uses its own deceleration curve. InfiniteCarouselView intercepts the swipe at the .decelerating phase transition — before any deceleration frame is rendered — and replaces it with a spring animation.

3. Tap-to-page across loop boundaries Tapping a visible card pages to that card. Boundary taps such as last-to-first and first-to-last are routed through the nearest equivalent branch in the tripled data, so the carousel moves one page across the loop instead of jumping across copies.


Requirements

  • iOS 18.0+
  • Swift 6.0+
  • Xcode 16.0+

Installation

Swift Package Manager

In Xcode: File → Add Package Dependencies

https://github.com/Minny27/InfiniteCarouselView.git

Or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/Minny27/InfiniteCarouselView.git", from: "1.0.2")
]

Usage

import InfiniteCarouselView

struct ContentView: View {
    @State private var selectedIndex = 0

    let items = [
        CardItem(id: 0, title: "First",  color: .orange),
        CardItem(id: 1, title: "Second", color: .blue),
        CardItem(id: 2, title: "Third",  color: .green),
    ]

    var body: some View {
        InfiniteCarouselView(
            items: items,
            spacing: 16,
            autoScrollInterval: 3,   // optional — omit to disable auto-scroll
            selectedIndex: $selectedIndex
        ) { item in
            // Draw your card at any size — InfiniteCarouselView measures it automatically
            RoundedRectangle(cornerRadius: 16)
                .fill(item.color)
                .frame(width: 280, height: 360)
                .overlay {
                    Text(item.title)
                        .font(.title2.bold())
                        .foregroundColor(.white)
                }
        }
    }
}

Parameters

Parameter Type Default Description
items [T: Identifiable] Data source
spacing CGFloat 16 Gap between cards
autoScrollInterval TimeInterval? nil Seconds between auto-advances. nil disables auto-scroll
selectedIndex Binding<Int> Currently centered card index (0-based, real items only)
content @ViewBuilder Card view. The size of the first rendered card is used as the step width

How It Works

Tripling strategy

Three copies of items are laid out side by side. Each copy has a unique id in the internal array, so SwiftUI never reuses or animates between them. After every scroll-settle (onScrollPhaseChange(.idle)), loopbackIfNeeded() checks whether displayIndex has entered a clone region and teleports back to the real region at the same visual position.

Synchronous snap target

InfiniteCarouselBehavior (a ScrollTargetBehavior) writes the resolved snap page into a shared SnapTarget class synchronously inside updateTarget, and also sets target.rect.origin.x so UIKit decelerates to the correct page. onScrollPhaseChange(.decelerating) reads that value in the same run-loop cycle and updates displayIndex / selectedIndex immediately, while UIKit handles the deceleration animation naturally.

Tap-to-page branch routing

When a card is tapped, InfiniteCarouselView resolves the tapped tripled index through InfiniteCarouselBranchParser. For normal taps, the target is used directly. For boundary taps, the parser first chooses a visually equivalent anchor in the opposite clone branch, then animates into the real section. This keeps the state flow ordered as branch resolution -> displayIndex sync -> scroll animation, avoiding visible backtracking at the loop edges.

Auto-scroll timer

The timer uses .task(id: scrollPhase). Every time scrollPhase changes (user touches the screen, programmatic scroll starts, etc.), the task is cancelled and restarted. This means the countdown always resets after any interaction, and the timer fires only when the scroll has been idle for the full interval.


License

InfiniteCarouselView is released under the MIT License. See LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages