Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Created July 25, 2024 14:17
Show Gist options
  • Save Codelaby/55cecad9fe220f976a710605a57cc054 to your computer and use it in GitHub Desktop.
Save Codelaby/55cecad9fe220f976a710605a57cc054 to your computer and use it in GitHub Desktop.
//
// CoverFlowCarousel.swift
// ClockSample
//
// Created by Codelaby on 25/7/24.
//
import SwiftUI
//RandomAccessCollection & MutableCollection & RangeReplaceableCollection & Equatable & Hashable
struct CoverFlowCarousel<Content: View, Data: RandomAccessCollection>: View where Data.Element: Identifiable {
@Environment(\.layoutDirection) var direction
private var config: Config
private let data: Data
@Binding var selection: Data.Element.ID?
private let content: (Data.Element) -> Content
init(data: Data, selection: Binding<Data.Element.ID?>, config: Config = Config(), @ViewBuilder content: @escaping (Data.Element) -> Content) {
self.data = data
self._selection = selection
self.config = config
self.content = content
}
struct Config {
var cornerRadius: CGFloat = 20
var spacing: CGFloat = 16
var cardWidth: CGFloat = 150
var minimumCardWidth: CGFloat = 40
var hasOpacity: Bool = true
var opacityValue: CGFloat = 0.4
var hasScale: Bool = true
var scaleValue: CGFloat = 0.2
}
var body: some View {
GeometryReader { geometry in
let size = geometry.size
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: config.spacing) {
ForEach(data, id: \.self.id) { item in
ItemView(item)
.id(item.id)
}
}
.scrollTargetLayout()
}
.safeAreaPadding(.horizontal, max((size.width - config.cardWidth) / 2, 0))
.scrollTargetBehavior(.viewAligned(limitBehavior: .always))
.scrollPosition(id: $selection)
}
}
@ViewBuilder
private func ItemView(_ item: Data.Element) -> some View {
GeometryReader { proxy in
let size = proxy.size
let minX = proxy.frame(in: .scrollView(axis: .horizontal)).minX * (direction == .rightToLeft ? -1 : 1)
let progress = minX / (config.cardWidth + config.spacing)
let minimumCardWidth = config.minimumCardWidth
let diffWidth = config.cardWidth - minimumCardWidth
let reducingWidth = progress * diffWidth
let cappedWidth = min(reducingWidth, diffWidth)
let resizedFrameWidth = size.width - (minX > 0 ? cappedWidth : min(-cappedWidth, diffWidth))
let negativeProgress = max(-progress, 0)
let opacityValue = config.opacityValue * abs(progress)
let scaleValue = config.scaleValue * abs(progress)
content(item)
//.frame(width: size.width, height: size.height)
.frame(width: resizedFrameWidth)
.clipShape(RoundedRectangle(cornerRadius: config.cornerRadius))
.offset(x: -reducingWidth)
.offset(x: min(progress, 1) * diffWidth)
.offset(x: negativeProgress * diffWidth)
.opacity(config.hasOpacity ? 1 - opacityValue : 1)
.scaleEffect(y: config.hasScale ? 1 - scaleValue : 1)
}
.frame(width: config.cardWidth)
}
}
#Preview {
struct PreviewWrapper: View {
struct MyLandmark: Identifiable, Hashable, Equatable {
var id: UUID = .init()
var image: String
var name: String
var color: Color = .red
}
let landmarks = [
MyLandmark(image: "Hongcun", name: "A", color: .red),
MyLandmark(image: "Kaifeng", name: "B", color: .green),
MyLandmark(image: "fenghuang", name: "C", color: .blue),
MyLandmark(image: "Jiangnan", name: "D", color: .purple),
MyLandmark(image: "Enshi", name: "E", color: .yellow),
MyLandmark(image: "ghizhou", name: "F", color: .orange)
]
@State private var currentIndex: UUID?
@State private var selectedLandmark: MyLandmark?
var body: some View {
NavigationStack {
VStack {
CoverFlowCarousel(data: landmarks, selection: $currentIndex,
config: .init(
cornerRadius: 20,
spacing: 16,
hasOpacity: false,
hasScale: false
)
) { landmark in
VStack {
//Image(landmark.image).aspectRatio(1.1, contentMode: .fill).frame(maxHeight: 240)
Rectangle().fill(landmark.color)
.overlay {
Text(landmark.name).padding(8).background(.thinMaterial, in: .circle)
}
}
.onTapGesture {
selectedLandmark = landmark
}
}
.frame(maxHeight: 240)
.navigationTitle("CoverFlow")
// .navigationDestination(isPresented: Binding(
// get: { selectedLandmark != nil },
// set: { if !$0 { selectedLandmark = nil } }
// )) {
// if let selectedLandmark = selectedLandmark {
// LandmarkDetailView(landmark: selectedLandmark)
// }
// }
Text("\(String(describing: selectedLandmark?.name))")
.font(.caption)
.padding(.top, 16)
Spacer()
}
}
}
}
struct LandmarkDetailView: View {
let landmark: PreviewWrapper.MyLandmark
var body: some View {
VStack {
Image(landmark.image)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
Text(landmark.name)
.font(.largeTitle)
.bold()
.padding()
Spacer()
}
.navigationTitle(landmark.name)
}
}
return PreviewWrapper()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment