Skip to content

Instantly share code, notes, and snippets.

@kirti-swiggy
Last active February 5, 2023 15:31
Show Gist options
  • Save kirti-swiggy/20bf26f4ba48fcfb878720e335c064dd to your computer and use it in GitHub Desktop.
Save kirti-swiggy/20bf26f4ba48fcfb878720e335c064dd to your computer and use it in GitHub Desktop.
A horizontal carousel view in SwiftUI with automatic and manual scroll support. Created using TabView.
//
// OfferCarousalView.swift
// Playground
//
// Created by Kirti Kumar Verma on 04/01/23.
//
import Combine
import Kingfisher
import SwiftUI
@available(iOS 14.0, *)
public struct OfferCarousalView: View {
public struct OfferData {
let heading: String
let subheading: String
let imageURL: URL
let onTap: () -> Void
public init(
heading: String,
subheading: String,
imageURL: URL,
onTap: @escaping () -> Void
) {
self.heading = heading
self.subheading = subheading
self.imageURL = imageURL
self.onTap = onTap
}
}
// MARK: State properties
@State private var selection: Int = 0
@State private var textBlockSize: CGSize?
// MARK: Theme
@ObservedObject private var themingObject: ThemeingObject
// MARK: Private properties
private let offers: [OfferData]
private let timer = Timer.publish(every: interval, on: .main, in: .default).autoconnect()
// MARK: - Public methods
public init(
themingObject: ThemeingObject,
offers: [OfferData]
) {
self.themingObject = themingObject
self.offers = offers
}
// MARK: - body
public var body: some View {
containerView
.background(Color(themingObject.color(for: \.backgroundPrimary).color))
.cornerRadius(Tokens.CornerRadius.large.value)
.shadow(
color: Color(Tokens.Elevation.raised300.value.color),
radius: Tokens.Elevation.raised300.value.radius,
x: Tokens.Elevation.raised300.value.offset.width,
y: Tokens.Elevation.raised300.value.offset.height
)
}
// MARK: - Implementation details
private var containerView: some View {
HStack(spacing: Tokens.Spacing.small.value) {
TabView(selection: $selection) {
ForEach(0 ..< numberOfItems, id: \.self) { _ in
offersTextBlock
.overlay(
GeometryReader { proxy in
Color.clear
.preference(key: ViewRectKey.self, value: proxy.size)
}
)
.onPreferenceChange(ViewRectKey.self) { size in
textBlockSize = size
}
}
}
.frame(height: textBlockSize?.height)
.tabViewStyle(.page(indexDisplayMode: .never))
indexDisplayView
}
.onReceive(timer) { _ in
withAnimation(.spring()) {
selection = (selection + 1) % numberOfItems
}
}
.padding(Tokens.Spacing.small.value)
.onTapGesture {
offers[selection].onTap()
}
}
private var offersTextBlock: some View {
HStack(spacing: Tokens.Spacing.xSmall.value) {
KFImage
.url(offers[selection].imageURL)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageDimention, height: imageDimention)
VStack(alignment: .leading, spacing: Tokens.Spacing.xxSmall.value) {
TextView(
themeingObject: themingObject,
text: offers[selection].heading,
textStyle: .bodyB1Bold,
textColorToken: \.textHighEmphasis
)
TextView(
themeingObject: themingObject,
text: offers[selection].subheading,
textStyle: .bodyB2Reg,
textColorToken: \.textMedEmphasis
)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var indexDisplayView: some View {
VStack(spacing: Tokens.Spacing.xxSmall.value) {
TextView(
text: "\(selection + 1)/\(numberOfItems)",
textStyle: .bodyB2Bold,
textColorToken: \.primary
)
HStack {
ForEach(0 ..< numberOfDots, id: \.self) { index in
Circle()
.frame(
width: getDotInfo(for: index).diameter,
height: getDotInfo(for: index).diameter
)
.foregroundColor(getDotInfo(for: index).color)
}
}
}
}
private var numberOfItems: Int {
offers.count
}
private var numberOfDots: Int {
min(maxNumberOfDots, numberOfItems)
}
private func getDotInfo(for index: Int) -> (diameter: CGFloat, color: Color) {
var isSelected: Bool = false
// MARK: TODO simplify the logic
if index == 0, selection == 0 {
isSelected = true
} else if index == numberOfDots - 1, selection == numberOfItems - 1 {
isSelected = true
} else if index == 1, selection > 0, selection < numberOfItems - 1 {
isSelected = true
}
if isSelected {
return (selectedDotDiameter, Color(colorToken: themingObject.color(for: \.primary).color))
} else {
return (unselectedDotDiameter, Color(colorToken: themingObject.color(for: \.borderSecondary).color))
}
}
private struct ViewRectKey: PreferenceKey {
static let defaultValue: CGSize? = nil
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
}
// MARK: Constants
private let maxNumberOfDots: Int = 3
private let interval: TimeInterval = 3
private let imageDimention: CGFloat = 48
private let selectedDotDiameter: CGFloat = 6
private let unselectedDotDiameter: CGFloat = 4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment