Last active
June 14, 2025 06:41
-
-
Save slowbrewedmacchiato/25cb04c7cadf627f39baebad72f084fc to your computer and use it in GitHub Desktop.
Creating iOS 26 Tab bars with separated option
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
/// The main application entry point for the Ancient Prophecy Tab Bars app. | |
/// | |
/// This app demonstrates a custom tab bar implementation with an overlay menu system. | |
/// Specifically the implementation of a separate tab bar option now available in iOS 26. | |
/// The app automatically exits when the main content view disappears and uses a fixed content size window. | |
@main | |
struct AncientProphecyTabBarsApp: App { | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
.onDisappear { | |
// Exit the app when the content view disappears | |
exit(0) | |
} | |
} | |
// Set window to resize based on content size only | |
.windowResizability(.contentSize) | |
} | |
} | |
// MARK: - Custom Tab Enum | |
/// Represents the available tabs in the custom tab bar interface. | |
/// | |
/// Each tab case provides its own title, SF Symbol, and unique identifier. | |
/// The enum conforms to multiple protocols to support SwiftUI's TabView and ForEach requirements. | |
enum CustomTab: Int, Equatable, Hashable, Identifiable, CaseIterable { | |
case watchNow | |
case library | |
case new | |
case favorites | |
case share | |
/// Unique identifier for each tab case. | |
/// | |
/// Uses the raw integer value as the identifier for Identifiable conformance. | |
var id: Int { | |
rawValue | |
} | |
/// Localized display title for each tab. | |
/// | |
/// Returns the appropriate localized string for display in the tab bar. | |
var title: String { | |
switch self { | |
case .watchNow: | |
return String(localized: "Watch Now", comment: "Tab title for immediate content viewing") | |
case .library: | |
return String(localized: "Library", comment: "Tab title for content library") | |
case .new: | |
return String(localized: "New", comment: "Tab title for new content discovery") | |
case .favorites: | |
return String(localized: "Favorites", comment: "Tab title for favorited content") | |
case .share: | |
return String(localized: "share", comment: "Tab title for sharing functionality") | |
} | |
} | |
/// SF Symbol name for each tab's icon. | |
/// | |
/// Returns the appropriate SF Symbol string for display in the tab bar. | |
var symbol: String { | |
switch self { | |
case .watchNow: | |
return "tv.fill" | |
case .library: | |
return "play.square.stack.fill" | |
case .new: | |
return "video.fill.badge.plus" | |
case .favorites: | |
return "heart.fill" | |
case .share: | |
return "square.and.arrow.up.fill" | |
} | |
} | |
/// Creates a view representation for the specified tab's content area. | |
/// | |
/// This static method generates a consistent layout for all tab content views, | |
/// displaying the tab's icon and title in a vertical stack. | |
/// | |
/// - Parameter tab: The tab case to create a view for | |
/// - Returns: A SwiftUI view containing the tab's icon and title | |
@ViewBuilder | |
static func makeTabView(for tab: CustomTab) -> some View { | |
if tab == .library { | |
ScrollView { | |
LazyVStack(alignment: .leading, spacing: 12) { | |
ForEach(0 ..< 100, id: \.self) { _ in | |
HStack { | |
Text(UUID().uuidString.prefix(18)) | |
.padding() | |
Spacer() | |
Image(systemName: "play.circle") | |
.padding() | |
} | |
} | |
} | |
.padding(.vertical) | |
} | |
} else { | |
VStack(spacing: 25) { | |
// Display the tab's SF Symbol icon | |
Image(systemName: tab.symbol) | |
.font(.largeTitle) | |
// Display the tab's localized title | |
Text(tab.title) | |
.font(.title) | |
} | |
} | |
} | |
} | |
// MARK: - ContentView | |
/// The main content view that implements a custom tab bar with overlay menu functionality. | |
/// | |
/// This view creates a TabView with custom tabs and implements a special overlay menu | |
/// that appears when the share tab is selected. The overlay menu provides additional | |
/// options without actually navigating to a share tab. | |
@available(iOS 26, *) | |
struct ContentView: View { | |
// MARK: - Properties | |
/// Array of all available tabs for the tab bar | |
let tabs: [CustomTab] = CustomTab.allCases | |
/// The currently selected tab in the tab bar | |
/// | |
/// This state property tracks which tab is currently active and updates the UI accordingly. | |
@State private var selectedTab: CustomTab = .watchNow | |
/// Controls the visibility of the overlay menu | |
/// | |
/// When true, displays the overlay menu with additional options. | |
/// This is triggered when the share tab is selected. | |
@State private var showOverlay: Bool = false | |
// MARK: - Body | |
var body: some View { | |
ZStack { | |
// MARK: - Main Tab View | |
TabView(selection: $selectedTab) { | |
ForEach(tabs) { tab in | |
Tab(value: tab, role: tab == CustomTab.share ? .search : .none) { | |
// Display the content view for each tab | |
ZStack { | |
CustomTab.makeTabView(for: tab) | |
} | |
} label: { | |
// Tab bar item with icon and title | |
Image(systemName: tab.symbol) | |
Text(tab.title) | |
} | |
} | |
} | |
.tint(Color(uiColor: .systemPink)) // Custom tint color for the tab bar | |
.tabBarMinimizeBehavior(.onScrollDown) // Hide tab bar when scrolling down | |
.onChange(of: selectedTab) { oldValue, newValue in | |
// Handle share tab selection by showing overlay instead of navigating | |
if newValue == .share { | |
showOverlay = true | |
// Revert to previous tab selection to prevent actual navigation | |
selectedTab = oldValue | |
} | |
} | |
// MARK: - Overlay Menu | |
if showOverlay { | |
// Semi-transparent background overlay | |
Color.black.opacity(0.0001) | |
.ignoresSafeArea() | |
.transition(.opacity) | |
.onTapGesture { | |
// Dismiss overlay when tapping outside the menu | |
withAnimation(.bouncy) { | |
showOverlay = false | |
} | |
} | |
// MARK: - Menu Content | |
GeometryReader { proxy in | |
VStack { | |
Spacer() | |
HStack { | |
Spacer() | |
// Menu container with options | |
VStack(alignment: .leading, spacing: 0) { | |
// Menu header | |
Text("Some Options") | |
.font(.headline) | |
.fontWeight(.bold) | |
.foregroundStyle(.primary) | |
.padding() | |
Divider() | |
// First menu option | |
Button { | |
// Handle first option selection | |
// TODO: Implement actual functionality | |
withAnimation { | |
showOverlay = false | |
} | |
} label: { | |
VStack(alignment: .leading) { | |
Text("Button Option 1") | |
.font(.headline) | |
.fontWeight(.medium) | |
.foregroundStyle(.primary) | |
Text("Subtitle option 1") | |
.font(.caption) | |
.foregroundStyle(.secondary) | |
} | |
.padding() | |
} | |
.buttonStyle(.plain) | |
Divider() | |
// Second menu option | |
Button { | |
// Handle second option selection | |
// TODO: Implement actual functionality | |
withAnimation { | |
showOverlay = false | |
} | |
} label: { | |
VStack(alignment: .leading) { | |
Text("Button Option 2") | |
.font(.headline) | |
.fontWeight(.medium) | |
.foregroundStyle(.primary) | |
Text("Subtitle option 2") | |
.font(.caption) | |
.foregroundStyle(.secondary) | |
} | |
.padding() | |
} | |
.buttonStyle(.plain) | |
} | |
.background(.ultraThinMaterial) // Translucent background material | |
.frame(width: proxy.size.width * 0.5) // Responsive width | |
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) | |
.padding(.horizontal, 24) | |
.padding(.bottom, 60) // Account for tab bar height | |
.shadow(radius: 2) // Subtle shadow for depth | |
.transition(.move(edge: .bottom).combined(with: .opacity)) | |
} | |
} | |
} | |
.animation(.bouncy, value: showOverlay) // Bouncy animation for menu appearance | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment