Last active
February 6, 2025 09:00
-
-
Save Codelaby/ef50289099a86325e9cbfb4c0852e831 to your computer and use it in GitHub Desktop.
InfiniteCarousel IOS18
This file contains 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
// | |
// CarouselInfinite.swift | |
// IOS18Playground | |
// | |
// Created by Codelaby on 28/9/24. | |
// | |
// based: https://www.youtube.com/watch?v=p1nN9eFOPNQ | |
import SwiftUI | |
// MARK: Sample model | |
struct ColorDemoModel: Identifiable, Hashable, Sendable { | |
let id: UUID = .init() | |
let title: String | |
let summary: String | |
let color: Color | |
static let allItems: [ColorDemoModel] = [ | |
ColorDemoModel(title: "Red", summary: "This is red", color: .red), | |
ColorDemoModel(title: "Green", summary: "This is green", color: .green), | |
ColorDemoModel(title: "Blue", summary: "This is blue", color: .blue), | |
ColorDemoModel(title: "Yellow", summary: "This is yellow", color: .yellow), | |
ColorDemoModel(title: "Orange", summary: "This is orange", color: .orange), | |
ColorDemoModel(title: "Pink", summary: "This is pink", color: .pink), | |
ColorDemoModel(title: "Mint", summary: "This is Mint", color: .mint), | |
ColorDemoModel(title: "Teal", summary: "This is teal", color: .teal), | |
ColorDemoModel(title: "Cyan", summary: "This is cyan", color: .cyan), | |
ColorDemoModel(title: "Indigo", summary: "This is indigo", color: .indigo), | |
ColorDemoModel(title: "Purple", summary: "This is purple", color: .purple), | |
ColorDemoModel(title: "Gray", summary: "This is Gray", color: .gray), | |
ColorDemoModel(title: "Brown", summary: "This is Brown", color: .brown) | |
// ColorItem(title: "Red", summary: "This is red", color: .red), | |
// ColorItem(title: "Green", summary: "This is green", color: .green), | |
// ColorItem(title: "Blue", summary: "This is blue", color: .blue), | |
// ColorItem(title: "Yellow", summary: "This is yellow", color: .yellow), | |
// ColorItem(title: "Orange", summary: "This is orange", color: .orange), | |
// ColorItem(title: "Pink", summary: "This is pink", color: .pink), | |
// ColorItem(title: "Mint", summary: "This is Mint", color: .mint), | |
// ColorItem(title: "Teal", summary: "This is teal", color: .teal), | |
// ColorItem(title: "Cyan", summary: "This is cyan", color: .cyan), | |
// ColorItem(title: "Indigo", summary: "This is indigo", color: .indigo), | |
// ColorItem(title: "Purple", summary: "This is purple", color: .purple), | |
// ColorItem(title: "Gray", summary: "This is Gray", color: .gray), | |
// ColorItem(title: "Brown", summary: "This is Brown", color: .brown) | |
] | |
} | |
// MARK: InfiniteCarousel | |
struct InfiniteCarousel<Content: View>: View { | |
@Binding var scrollPosition: Int | |
@ViewBuilder var content: Content | |
@State private var isScrolling: Bool = false | |
var body: some View { | |
ScrollView(.horizontal, showsIndicators: false) { | |
Group(subviews: content) { subviews in | |
if subviews.isEmpty { | |
Text("no views collection") | |
} else { | |
LazyHStack(spacing: 0) { | |
if let lastItem = subviews.last { | |
lastItem | |
.containerRelativeFrame(.horizontal) | |
.id(-1) | |
.onChange(of: isScrolling) { oldValue, newValue in | |
if !newValue && scrollPosition == -1 { | |
scrollPosition = subviews.count - 1 | |
} | |
} | |
} | |
ForEach(Array(subviews.enumerated()), id: \.element.id) { index, subview in | |
subview | |
.containerRelativeFrame(.horizontal) | |
.id(index) | |
} | |
if let firstItem = subviews.first { | |
firstItem | |
.containerRelativeFrame(.horizontal) | |
.id(subviews.count) | |
.onChange(of: isScrolling) { oldValue, newValue in | |
if !newValue && scrollPosition == subviews.count { | |
scrollPosition = 0 | |
} | |
} | |
} | |
} | |
.scrollTargetLayout() | |
} | |
} | |
} | |
.scrollTargetBehavior(.paging) | |
.onScrollPhaseChange { oldPhase, newPhase in | |
isScrolling = newPhase.isScrolling | |
} | |
.scrollPosition(id: .init( | |
get: { scrollPosition }, | |
set: { value, _ in | |
if let value { | |
scrollPosition = value | |
} | |
} | |
)) | |
.onChange(of: scrollPosition, initial: true, { oldValue, newValue in | |
if oldValue == newValue { | |
DispatchQueue.main.asyncAfter(deadline: .now() ) { | |
scrollPosition = newValue | |
//scrollViewProxy.scrollTo(newValue * 10, anchor: .center) | |
} | |
} | |
}) | |
} | |
} | |
// MARK: DotPageIndicator | |
struct DotPageIndicator: View { | |
let currentIndex: Int | |
let max: Int | |
private var adjustedIndex: Int { | |
if currentIndex < 0 { | |
return max | |
} else if currentIndex > max { | |
return 0 | |
} else { | |
return currentIndex | |
} | |
} | |
init(currentIndex: Int, max: Int) { | |
self.currentIndex = currentIndex | |
self.max = max - 1 | |
} | |
var body: some View { | |
HStack { | |
ForEach(Array(0..<max + 1), id: \.self) { index in | |
Circle() | |
.fill(index == adjustedIndex ? Color.accentColor : Color.accentColor.mix(with: .white, by: 0.5)) | |
.frame(width: 5, height: 5) | |
} | |
} | |
.padding(4) | |
.background(.thinMaterial, in: .capsule) | |
} | |
} | |
// MARK: Playground | |
struct CarouselDemo: View { | |
@State var currentIndex: Int = 6 | |
var body: some View { | |
VStack { | |
InfiniteCarousel(scrollPosition: $currentIndex) { | |
ForEach(ColorDemoModel.allItems) { item in | |
RoundedRectangle(cornerRadius: 16, style: .continuous) | |
.fill(item.color.gradient) | |
.padding(.horizontal) | |
.overlay { | |
VStack { | |
Text(item.title) | |
.font(.title) | |
.fontWeight(.bold) | |
.foregroundStyle(.white) | |
Text(item.summary) | |
.font(.subheadline) | |
.foregroundStyle(.white) | |
} | |
} | |
.onTapGesture { | |
print("tap", item) | |
} | |
} | |
} | |
.frame(height: 240) | |
DotPageIndicator(currentIndex: currentIndex, max: ColorDemoModel.allItems.count) | |
} | |
} | |
} | |
#Preview { | |
VStack { | |
SampleTitleView(title: "Carousel infinite in SwiftUI", summary: "reusable carousel with dot page indicador") | |
Spacer() | |
CarouselDemo() | |
.frame(maxHeight: 280) | |
Spacer() | |
CreditsView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment