skip to Main Content

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:

First Image

But I want it to change to 2nd Image kind of:

Second Image

2

Answers


  1. 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:

    Animated Dot Indicator

    If you are looking to support older iOS versions you can check this other video of his.

    Let me know your thoughts!

    Login or Signup to reply.
  2. The easiest way to do this is to use the current index. Based on the index, you can adjust the size of the views with the animation you showed us.

    struct DotIndicatorView: View {
       
         @ObservedObject var sharedState: DotIndicatorViewState
    
          var body: some View {
            HStack {
                ForEach(0 ..< sharedState.numberOfDots, id: .self) { index in
                    Capsule()
                        .fill(blue(index == sharedState.currentIndex ? 1 : 0.5))
                        .frame(width: index == sharedState.currentIndex ? 20 : 10, height: 10)
                        .transition(.slide)
                        .animation(.easeInOut, value: sharedState.currentIndex)
                }
            }
            .padding()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search