Contents

lynnswap/scrollabletabbar

ScrollableTabBar is an iOS UIKit control for presenting an ordered selection

Requirements

  • iOS 18.4+
  • Swift 6.3+

State Ownership

The app remains the source of truth for each item's domain identity, membership and order, selected value, and corresponding content. ScrollableTabBar projects that selection into UIKit. Apply changes to membership, order, display content, and selection together with setItems(_:selectedID:).

See Selection Ownership for programmatic and user-driven selection behavior.

Quick Start

import ScrollableTabBar
import UIKit

@MainActor
final class DashboardViewController: UIViewController, ScrollableTabBarDelegate {
    enum Section: Hashable {
        case summary
        case activity
        case settings
    }

    private var selectedSection: Section = .summary

    private lazy var sectionControl: ScrollableTabBar<Section> = {
        let control = ScrollableTabBar(
            items: [
                .init(id: .summary, title: "Summary"),
                .init(id: .activity, title: "Activity"),
                .init(
                    id: .settings,
                    title: "Settings",
                    image: UIImage(systemName: "gear")
                ),
            ],
            selectedID: selectedSection
        )
        control.accessibilityLabel = "Dashboard Section"
        control.delegate = self
        return control
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        sectionControl.sizeToFit()
        navigationItem.titleView = sectionControl
        showSection(selectedSection)
    }

    func scrollableTabBar(
        _ tabBar: ScrollableTabBar<Section>,
        didSelect selectedID: Section
    ) {
        selectedSection = selectedID
        showSection(selectedID)
    }

    private func showSection(_ section: Section) {
        // Render application content for `section`.
    }
}

Updating Tabs

Pass the application's updated items and selection to the same control:

var items: [ScrollableTabBar<String>.Item] = [
    .init(id: "inbox", title: "Inbox"),
    .init(id: "archive", title: "Archive"),
]
let tabBar = ScrollableTabBar(items: items, selectedID: "inbox")

items.append(.init(id: "drafts", title: "Drafts"))
tabBar.setItems(items, selectedID: "drafts")

items.removeAll { $0.id == "drafts" }
tabBar.setItems(items, selectedID: "inbox")

tabBar.setItems([], selectedID: nil)

Item IDs must be unique. A non-nil selection must belong to the supplied items; nil means no selection. Programmatic updates do not call the delegate. ScrollableTabBar<ID>() creates an empty control that can be populated later.

selectedID is now optional. When migrating existing code that reads it, handle the absence of a selection before rendering the selected content.

Presentation

When its expected UIKit runtime contract is available, the control uses the system floating-tab presentation. Otherwise, a public UIKit fallback preserves selection, ordering, enablement, and event semantics. Exact layout and appearance are not API guarantees.

Testing

Run package tests on an iOS Simulator:

xcodebuild test \
  -workspace ScrollableTabBar.xcworkspace \
  -scheme ScrollableTabBarTests \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest'

Run the external public-product contract from its separate package:

cd ContractTests
xcodebuild test \
  -scheme ScrollableTabBarProductContract-Package \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest'

Documentation

License

ScrollableTabBar is available under the MIT License. See LICENSE.

Package Metadata

Repository: lynnswap/scrollabletabbar

Default branch: main

README: README.md