Created
February 24, 2025 10:46
-
-
Save Codelaby/58afd1fe01187d2ff3cab5a1e46c8d6d to your computer and use it in GitHub Desktop.
Slime Progress Dot Indicator
This file contains hidden or 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
| // 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