TKRubberPageControl

September 14, 2026 · View on GitHub

A rubber animation page control for UIKit and SwiftUI.

Demo

English | 中文说明

Features

  • The original rubber animation: the big bubble travels between pages, the small bubbles hop along a half circle under the bar and squash in flight.
  • A UIControl based UIKit implementation, source compatible with 1.x.
  • A native, Binding driven SwiftUI implementation with no UIViewRepresentable.
  • Both implementations share the same layout model, so they look identical.
  • Style configuration, dark mode support and basic accessibility.

Requirements

  • iOS 13.0+
  • Swift 5.9+
  • Xcode 15+

Installation

Swift Package Manager

Add the package in Xcode via File → Add Package Dependencies:

https://github.com/TBXark/TKRubberIndicator

Or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/TBXark/TKRubberIndicator.git", from: "2.0.0")
]

CocoaPods

pod 'TKRubberPageControl', '~> 2.0'

Usage

UIKit

import TKRubberPageControl
import UIKit

final class ViewController: UIViewController {

    private let pageControl = TKRubberPageControl(frame: .zero, count: 5)

    override func viewDidLoad() {
        super.viewDidLoad()

        pageControl.translatesAutoresizingMaskIntoConstraints = false
        pageControl.addTarget(
            self,
            action: #selector(pageChanged(_:)),
            for: .valueChanged
        )
        pageControl.valueChange = { index in
            print("page is \(index)")
        }

        view.addSubview(pageControl)
        NSLayoutConstraint.activate([
            pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pageControl.widthAnchor.constraint(equalToConstant: 220),
            pageControl.heightAnchor.constraint(equalToConstant: 100),
        ])
    }

    @objc private func pageChanged(_ sender: TKRubberPageControl) {
        print("page is \(sender.currentIndex)")
    }
}

Assigning currentIndex animates and fires the events too. Assigning numberOfPage or styleConfig rebuilds the indicator and resets the selection to 0:

pageControl.currentIndex = 2   // animates to page 2
pageControl.numberOfPage = 4   // rebuilds, back to page 0

SwiftUI

import SwiftUI
import TKRubberPageControl

struct ContentView: View {
    @State private var page = 0

    var body: some View {
        RubberPageControl(selection: $page, pageCount: 5)
            .frame(width: 220, height: 100)
    }
}

The selection lives in the binding, so the control updates itself whenever the state changes and writes back when the user taps it. On iOS 15 and later those changes animate automatically; on iOS 13 and 14 wrap the assignment in withAnimation:

withAnimation {
    page = 3
}

Customization

TKRubberPageControlConfig and RubberPageControlStyle expose the same options; both default to the colours and sizes of the 1.x releases.

OptionDescriptionDefault
smallBubbleSizeDiameter of the small bubbles16
mainBubbleSizeDiameter of the bump behind the selected bubble40
bubbleXOffsetSpace / bubbleSpacingSpacing between bubbles12
bubbleYOffsetSpace / verticalPaddingVertical padding of the bar8
animationDurationPage change animation duration0.2
backgroundColor / barColorBar and bump colorplum
smallBubbleColorSmall bubble colorcoral
bigBubbleColorBig bubble colorcrimson

UIKit:

var config = TKRubberPageControlConfig()
config.backgroundColor = .systemIndigo
config.bigBubbleColor = .systemYellow
let pageControl = TKRubberPageControl(frame: frame, count: 5, config: config)

SwiftUI:

RubberPageControl(
    selection: $page,
    pageCount: 5,
    style: RubberPageControlStyle(barColor: .indigo, bigBubbleColor: .yellow)
)

Demo

The demo app shows the UIKit and the SwiftUI control side by side. It is generated with XcodeGen and consumes the library as a local Swift package:

./Scripts/demo.sh

Or run make demo.

License

TKRubberPageControl is available under the MIT license. See LICENSE.