I want to change the shape of the dot to a rounded rectangle which ever tab is selected.
First Image is the current dot style paging:
But I want it to change to 2nd Image kind of:
Thaks to YouTuber Kavsoft and to his video we know how to do it.
This View allows for some small customisation, you'll need to get your hand dirty if you want more, of course, and it is only compatible starting from iOS 17
. Here's the code:
struct PagingIndicator: View {
/// Customization properties
var activeTint: Color = .primary
var inactiveTint: Color = .primary.opacity(0.15)
var opacityEffect: Bool = false
var clipEdges: Bool = false
var body: some View {
let hstackSpacing: CGFloat = 10
let dotSize: CGFloat = 8
let spacingAndDotSize = hstackSpacing + dotSize
GeometryReader {
let width = $0.size.width
/// ScrollView boounds
if let scrollViewWidth = $0.bounds(of: .scrollView(axis: .horizontal))?.width,
scrollViewWidth > 0 {
let minX = $0.frame(in: .scrollView(axis: .horizontal)).minX
let totalPages = Int(width / scrollViewWidth)
/// Progress
let freeProgress = -minX / scrollViewWidth
let clippedProgress = min(max(freeProgress, 0), CGFloat(totalPages - 1))
let progress = clipEdges ? clippedProgress : freeProgress
/// Indexes
let activeIndex = Int(progress)
let nextIndex = Int(progress.rounded(.awayFromZero))
let indicatorProgress = progress - CGFloat(activeIndex)
/// Indicator width (Current & upcoming)
let currentPageWidth = spacingAndDotSize - (indicatorProgress * spacingAndDotSize)
let nextPageWidth = indicatorProgress * spacingAndDotSize
HStack(spacing: hstackSpacing) {
ForEach(0..<totalPages, id: \.self) { index in
Capsule()
.fill(inactiveTint)
.frame(width: dotSize + ((activeIndex == index) ? currentPageWidth : (nextIndex == index) ? nextPageWidth : 0),
height: dotSize)
.overlay {
ZStack {
Capsule()
.fill(inactiveTint)
Capsule()
.fill(activeTint)
.opacity(opacityEffect ?
(activeIndex == index) ? 1 - indicatorProgress : (nextIndex == index) ? indicatorProgress : 0
: 1
)
}
}
} //: LOOP DOTS
} //: HSTACK
.frame(width: scrollViewWidth)
.offset(x: -minX)
}
} //: GEOMETRY
.frame(height: 30)
}
}
The clipEdges
Boolean property avoid the animation when reaching the end of the carousel when set to true in case you are wondering.
You can use it like this:
ScrollView(.horizontal) {
LazyHStack(spacing: 0) {
ForEach(colors, id: \.self) { color in
RoundedRectangle(cornerRadius: 25)
.fill(color.gradient)
.padding(.horizontal, 5)
.containerRelativeFrame(.horizontal)
} //: LOOP COLORS
} //: LAZY HSTACK
.scrollTargetLayout() /// Comment to have standrd Paging behaviour
.overlay(alignment: .bottom) {
PagingIndicator(
activeTint: .white,
inactiveTint: .black.opacity(0.25),
opacityEffect: opacityEffect,
clipEdges: clipEdges
)
}
} //: SCROLL
.scrollIndicators(.hidden)
.frame(height: 220)
/// Uncomment these two lines to have the standard paging
//.padding(.top, 15)
//.scrollTargetBehavior(.paging)
.safeAreaPadding(.vertical, 15)
.safeAreaPadding(.horizontal, 25)
.scrollTargetBehavior(.viewAligned)
Where colors
is an array of colors:
@State private var colors: [Color] = [.red, .blue, .green, .yellow]
Here's the result:
If you are looking to support older iOS versions you can check this other video of his.
Let me know your thoughts!