Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Created February 24, 2025 10:46
Show Gist options
  • Select an option

  • Save Codelaby/58afd1fe01187d2ff3cab5a1e46c8d6d to your computer and use it in GitHub Desktop.

Select an option

Save Codelaby/58afd1fe01187d2ff3cab5a1e46c8d6d to your computer and use it in GitHub Desktop.
Slime Progress Dot Indicator
// MARK: Slime Progress Dot Indicator
struct SlimeProgressDotPageIndicator: View {
private let currentPage: Int
private let numberOfPages: Int
private let hidesForSinglePage: Bool
private let config: Config
private let progress: CGFloat
private var adjustedIndex: Int {
return currentPage < 0 ? numberOfPages : (currentPage > numberOfPages ? 0 : currentPage)
}
struct Config {
var dotSize: CGFloat = 8
var pageIndicatorHighlight: Color = .secondary
var pageIndicatorNext: Color = .secondary
var pageIndicatorLast: Color = .secondary
}
init(currentPage: Int, numberOfPages: Int, progress: CGFloat, hidesForSinglePage: Bool = true, config: Config = Config()) {
self.currentPage = currentPage
self.numberOfPages = numberOfPages - 1
self.progress = min(max(progress, 0), 1)
self.hidesForSinglePage = hidesForSinglePage
self.config = config
}
var body: some View {
HStack(spacing: 10) {
ForEach(Array(0..<numberOfPages + 1), id: \.self) { index in
Capsule()
.fill(index == adjustedIndex ? config.pageIndicatorHighlight : index < currentPage ? config.pageIndicatorLast : config.pageIndicatorNext)
.frame(width: index == currentPage ? config.dotSize * 2.5 : config.dotSize, height: config.dotSize)
.transition(.slide)
.animation(.easeInOut, value: currentPage)
.overlay(alignment: .leading) {
if index == currentPage && progress != 0 {
Capsule()
.frame(width: index == currentPage ? (config.dotSize * 2.5) * progress : config.dotSize, height: config.dotSize)
.animation(.easeInOut, value: progress)
}
}
}
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(.thickMaterial, in: .capsule)
.hidden(numberOfPages == 0 ? hidesForSinglePage : false)
}
}
// MARK: Preview
#Preview("Slime Progress Dot Indicator") {
@Previewable @State var selectedTab = 0
@Previewable @State var progress: CGFloat = 0
let maxPages: Int = 5
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
VStack {
TabView(selection: $selectedTab) {
ForEach(0..<maxPages, id: \.self) { index in
VStack {
Text("Page \(index + 1)")
.font(.largeTitle)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: 280)
.background(Color.blue.hueRotation(.degrees(Double(index) * 36)))
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
SlimeProgressDotPageIndicator(currentPage: selectedTab, numberOfPages: maxPages, progress: progress)
}
.onChange(of: selectedTab, { oldValue, newValue in
progress = 0
})
.onReceive(timer) { _ in
progress += 0.1
if progress >= 1.0 {
withAnimation {
selectedTab = (selectedTab + 1) % maxPages
}
progress = 0
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment