Skip to content

camosss/TabPagerX

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

92 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

TabPagerX

Swift Version Release Version SPM CocoaPods

Effortless SwiftUI tab pager with dynamic customization.

TabPagerX is a SwiftUI-based library designed to help iOS developers create customizable tab pagers with ease. It offers flexible layouts, tab scroll preservation, and extensive styling options for tab buttons and indicators, making it a perfect choice for building tab-based navigation in your SwiftUI applications.


๐Ÿ’ฅ Features

  • Generic Data API: Work with any Identifiable & Equatable data model.
  • Type-safe Builders: Closure-based content and tabTitle per item.
  • Static & Dynamic Tabs: Supports both fixed arrays and API-driven dynamic lists โ€” safe with empty or async-loaded items.
  • Configurable Layouts: Fixed/Scrollable tab bar with spacing and padding controls.
  • Indicator Customization: Height, color, corner radius, horizontal inset, animation.
  • Real-time Indicator Tracking: Indicator follows your finger in real-time during swipe.
  • Optional Separator: Built-in separator between TabBar and content via modifier.
  • Gesture Navigation: Enable/disable swipe between pages. Disabling swipe also removes tab transition animation.

๐Ÿ’ฅ Requirements

  • iOS 15.0+
  • Swift 5.5+
  • Xcode 15.0+

๐Ÿ’ฅ Usage

Getting Started

  • Bind a @State variable to selectedIndex to track the current tab.
  • Optionally set initialIndex (default is 0) to define which tab is shown first (applied once on first appearance).
  • Provide items (any Identifiable & Equatable type).
  • Define each tab's content using SwiftUI views via content closure.
  • Use tabTitle closure to provide a custom tab label per item (@ViewBuilder).
@State private var selectedIndex = 0
private let items = [..]

TabPagerX(
    selectedIndex: $selectedIndex,
    items: items
) { item in
    /* content */
} tabTitle: { item, isSelected in
    /* title */
}

Same Content (all items share the same view)

  • Ideal for simple static lists or repeating the same layout.
  • All tabs use the same view structure with different data.
Same Content Example 1 Same Content Example 2
struct TabItem: Identifiable, Equatable {
    let id = UUID()
    let title: String
    let content: String
    let color: Color
}

@State private var selectedIndex = 0

private let items = [
    TabItem(title: "Home", content: "Welcome to Home", color: .blue),
    TabItem(title: "Search", content: "Search content", color: .green),
    TabItem(title: "Profile", content: "Profile content", color: .orange)
]

TabPagerX(
    selectedIndex: $selectedIndex,
    items: items
) { item in
    VStack {
        Text(item.content)
            .font(.title2)
            .foregroundColor(item.color)
        Rectangle()
            .fill(item.color)
            .frame(height: 200)
            .cornerRadius(12)
    }
    .padding()

} tabTitle: { item, isSelected in
    Text(item.title)
        .font(isSelected ? .headline : .body)
        .foregroundColor(isSelected ? item.color : .secondary)
        .padding(.horizontal, 16)
        .padding(.vertical, 8)
}
.tabBarLayoutStyle(.fixed)
.tabIndicatorStyle(height: 3, color: .blue, horizontalInset: 16)

Different Views by Type (render different view per type)

  • Renders different views based on each item's type.
  • Useful when each tab needs heterogeneous UI.
Different Views - Image 1 Different Views - Image 2
struct MixedTabItem: Identifiable, Equatable {
    let id = UUID()
    let type: TabItemType
    let title: String

    enum TabItemType: Equatable {
        case text(String)
        case image(String)
        case custom
    }
}

@State private var selectedIndex = 0

private let items = [
    MixedTabItem(type: .text("Hello World"), title: "Text"),
    MixedTabItem(type: .image("star.fill"), title: "Image"),
    MixedTabItem(type: .custom, title: "Custom")
]

TabPagerX(
    selectedIndex: $selectedIndex,
    items: items
) { item in
    switch item.type {
    case .text(let text):
        Text(text)
            .font(.largeTitle)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    case .image(let name):
        Image(systemName: name)
            .font(.system(size: 60))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    case .custom:
        Circle()
            .fill(LinearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))
            .frame(width: 100, height: 100)
    }

} tabTitle: { item, isSelected in
    HStack {
        if case .image = item.type {
            Image(systemName: "photo")
        } else if case .custom = item.type {
            Image(systemName: "star.circle")
        }
        Text(item.title)
    }
    .font(isSelected ? .headline : .body)
    .foregroundColor(isSelected ? .blue : .secondary)
    .padding(.horizontal, 12)
    .padding(.vertical, 8)
}
.tabIndicatorStyle(height: 4, color: .purple)

Dynamic / Async Tabs

  • Safe with empty or async-loaded items โ€” no isLoading guard needed.
  • Tabs render automatically when data arrives.
@State private var selectedIndex = 0
@State private var items: [Item] = [] // starts empty

var body: some View {
    VStack {
        Button("Reload") { loadData() }

        // No isLoading guard needed โ€” safe with empty items
        TabPagerX(
            selectedIndex: $selectedIndex,
            initialIndex: 1, // applied once when items load
            items: items
        ) { item in
            Text(item.content)
                .font(.title2)
                .frame(maxWidth: .infinity, maxHeight: .infinity)

        } tabTitle: { item, isSelected in
            HStack {
                Image(systemName: item.icon)
                Text(item.title)
            }
            .font(isSelected ? .headline : .body)
            .foregroundColor(isSelected ? item.color : .secondary)
            .padding(.horizontal, 12)
            .padding(.vertical, 8)
        }
        .tabBarLayoutStyle(.scrollable)
        .tabIndicatorStyle(height: 3, color: .green, horizontalInset: 8)
    }
    .onAppear { loadData() }
}

func loadData() {
    items = []
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        items = [/* fetched data */]
    }
}

Scrollable Tabs (many tabs with real-time indicator)

  • Scrollable layout for many tabs with button spacing and side padding.
  • Indicator and tab title selection follow your finger in real-time during swipe.
@State private var selectedIndex = 0

private let items = [
    CategoryItem(title: "All", emoji: "๐ŸŒ", color: .blue),
    CategoryItem(title: "Music", emoji: "๐ŸŽต", color: .pink),
    CategoryItem(title: "Sports", emoji: "โšฝ", color: .green),
    CategoryItem(title: "Gaming", emoji: "๐ŸŽฎ", color: .purple),
    CategoryItem(title: "Food", emoji: "๐Ÿ•", color: .orange),
    CategoryItem(title: "Travel", emoji: "โœˆ๏ธ", color: .cyan),
    // ...
]

TabPagerX(
    selectedIndex: $selectedIndex,
    items: items
) { item in
    VStack(spacing: 16) {
        Text(item.emoji).font(.system(size: 80))
        Text(item.title).font(.title).foregroundColor(item.color)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)

} tabTitle: { item, isSelected in
    Text("\(item.emoji) \(item.title)")
        .font(isSelected ? .headline : .subheadline)
        .foregroundColor(isSelected ? item.color : .secondary)
        .padding(.horizontal, 14)
        .padding(.vertical, 8)
}
.tabBarLayoutStyle(.scrollable)
.tabBarLayoutConfig(buttonSpacing: 4, sidePadding: 12)
.tabIndicatorStyle(height: 3, color: .blue, cornerRadius: 1.5)

Swipe Disabled (instant tab switch)

  • When swipe is disabled, tapping a tab switches content instantly with no slide animation.
TabPagerX(
    selectedIndex: $selectedIndex,
    items: items
) { item in
    Text(item.title)
        .font(.largeTitle)
        .frame(maxWidth: .infinity, maxHeight: .infinity)

} tabTitle: { item, isSelected in
    Text(item.title)
        .font(isSelected ? .headline : .body)
        .foregroundColor(isSelected ? item.color : .secondary)
        .padding(.horizontal, 16)
        .padding(.vertical, 8)
}
.tabBarLayoutStyle(.fixed)
.tabIndicatorStyle(height: 3, color: .red)
.contentSwipeEnabled(false) // no swipe, no slide animation on tap

For more examples, see TabPagerXSample in the sample app. (link: TabPagerXSample)


๐Ÿ’ฅ Configuration

layoutStyle

  • Set Tab Bar Layout Style.
  • Choose between fixed or scrollable layouts.
  • Custom tab views are fully supported in both layouts.
.tabBarLayoutStyle(.fixed)
Fixed layout screenshot
.tabBarLayoutStyle(.scrollable)
Scrollable layout screenshot
// Fixed: tabs share equal width across the screen (default)
.tabBarLayoutStyle(.fixed)

// Scrollable: tabs size to content, horizontally scrollable
.tabBarLayoutStyle(.scrollable)

layoutConfig

  • Configure Tab Bar Layout.
  • Adjust buttonSpacing and sidePadding. (defaults to 0)
    • buttonSpacing: spacing between each tab button
    • sidePadding: horizontal padding applied to the whole tab bar (left & right)
// No spacing (default)
.tabBarLayoutConfig(buttonSpacing: 0, sidePadding: 0)

// With spacing and padding
.tabBarLayoutConfig(buttonSpacing: 8, sidePadding: 12)

indicatorStyle

  • Customize Tab underline (indicator) with .tabIndicatorStyle(...).
  • You can set height, color, horizontalInset, cornerRadius, and animationDuration.
  • The indicator tracks your finger in real-time during swipe gestures.
// Thin blue underline
.tabIndicatorStyle(height: 2, color: .blue)

// Rounded pill with inset
.tabIndicatorStyle(
    height: 4,
    color: .orange,
    horizontalInset: 20,
    cornerRadius: 2,
    animationDuration: 0.25
)

// No indicator (default โ€” height: 0, color: .clear)

isSwipeEnabled

  • Enable or Disable Content Swipe.
  • Allow or disable swipe gesture to switch between tabs.
  • Default is true. Use .contentSwipeEnabled(false) to disable swipe navigation.
  • When disabled, tab tap transitions are also instant (no slide animation).
// Swipe enabled (default) โ€” swipe between pages with slide animation
.contentSwipeEnabled(true)

// Swipe disabled โ€” tap only, instant content switch
.contentSwipeEnabled(false)

separatorStyle

  • Adds a separator line between the TabBar and the content area.
  • Use to visually distinguish the tab bar from page content.
// Add separator
.tabBarSeparator(
    color: .gray.opacity(0.3),
    height: 1
)

// Full customization
.tabBarSeparator(
    color: .gray.opacity(0.2),
    height: 1,
    horizontalPadding: 16,
    isHidden: false
)
Separator ON Separator OFF

onTabChanged

  • Observe tab index changes via callback.
.onTabChanged { index in
    print("Selected tab: \(index)")
}

๐Ÿ’ฅ Installation

SPM

In Xcode, go to File > Add Packages

https://github.com/camosss/TabPagerX.git

CocoaPods

Add to your Podfile

pod 'TabPagerX'

Run

pod install

๐Ÿ’ฅ License

TabPagerX is released under an MIT license. See License for more information.

About

Effortless SwiftUI tab pager with dynamic customization

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors