# TabView Architecture

Part of the `swiftui-navigation` skill. Goal: tabs that scale across platforms,
with one source of truth for what each tab is and shows, different tab sets and
sidebar groupings per platform, tabs generated from data, and a hook that can
intercept special tabs.

## Building blocks

- `AppTab` enum: identity, title, icon and a builder for its content.
- `SidebarSections` enum: which tabs appear under which sidebar heading. Give
  its `title` the type `LocalizedStringKey`. With a `String` title,
  `TabSection(group.title)` makes `Tab(value:)` ambiguous and the view does not
  compile.
- `AppView`: owns the `TabView` and the selection, and routes selection changes
  through `updateTab(with:)`.

## Selection with side effects

```swift
struct AppView: View {
    let sections: [SidebarSections]
    @State private var scrollTarget: AppTab?
    @State private var showsComposer = false
    @State private var selectedTab: AppTab = .home

    private var selection: Binding<AppTab> {
        Binding(get: { selectedTab }, set: updateTab(with:))
    }

    var body: some View {
        TabView(selection: selection) {
            ForEach(sections) { group in
                TabSection(group.title) {
                    ForEach(group.tabs) { entry in
                        Tab(value: entry) {
                            entry.makeContentView(scrollTarget: $scrollTarget)
                        } label: {
                            entry.label
                        }
                        .tabPlacement(entry.tabPlacement)
                    }
                }
                .tabPlacement(.sidebarOnly)
            }
        }
        .sheet(isPresented: $showsComposer) { ComposerView() }
    }

    private func updateTab(with next: AppTab) {
        if next == .post {
            showsComposer = true
            return
        }
        if next == selectedTab {
            scrollTarget = next
        }
        selectedTab = next
    }
}
```

Choosing `.post` opens the composer and leaves the selection where it was.
Choosing the current tab again asks it to scroll to the top.

## Selection without side effects

If selection is pure state, bind directly: `TabView(selection: $selectedTab)`
with the same `TabSection` and `Tab` structure inside.

## Rules worth keeping

- `AppTab` is the single home for identity and content, through
  `makeContentView(...)`.
- Tabs use `Tab(value:)` together with a selection binding.
- Selection goes through `updateTab(with:)`; that one function deals with
  compose-style tabs and with re-tapping to scroll up.
- Sidebar structure comes from `TabSection` plus `.tabPlacement(.sidebarOnly)`.
- `.tabPlacement(.pinned)` keeps one tab fixed in place. It is typical for an
  iOS 26 `.searchable` tab but works for any tab.

## Tabs generated from data

`SidebarSections` can hold tabs that come from user data, such as saved
filters. Represent them with one case that carries a payload, for example
`AppTab.savedFilter(SavedFilter)`, and read the title and icon from the filter.

```swift
enum AppTab: Hashable, Identifiable {
    case home, search, post, profile
    case savedFilter(SavedFilter)

    var id: String {
        guard case .savedFilter(let filter) = self else { return String(describing: self) }
        return "filter-\(filter.id)"
    }

    @ViewBuilder var label: some View {
        switch self {
        case .savedFilter(let filter): Label(filter.name, systemImage: filter.symbol)
        case .home: Label("Start", systemImage: "house")
        case .search: Label("Search", systemImage: "magnifyingglass")
        case .post: Label("Post", systemImage: "square.and.pencil")
        case .profile: Label("Profile", systemImage: "person")
        }
    }
}
```

## Tab APIs from iOS 18 and iOS 26

```swift
TabView {
    Tab("Feed", systemImage: "list.bullet") { FeedView() }
    Tab(role: .search) { SearchView() }
}
.tabBarMinimizeBehavior(.onScrollDown)
.tabViewBottomAccessory { MiniPlayerBar() }
.tabViewSidebarHeader { AccountHeader() }
.tabViewSidebarFooter { StorageFooter() }
.tabViewSidebarBottomBar { SettingsButton() }
```

iOS 18 (macOS 15, visionOS 2):

- `Tab(role: .search)` marks the search tab; the iOS 26 tab bar replaces the bar
  with a search field while that tab is active.
- Sidebar customisation: `.tabViewSidebarHeader { }`,
  `.tabViewSidebarFooter { }` and `.tabViewSidebarBottomBar { }`.
- `TabSection` and `.tabPlacement(_:)`.

iOS 26 only:

- `TabBarMinimizeBehavior`: `.automatic` (decided by context), `.onScrollDown`,
  `.onScrollUp` and `.never`. The two scroll behaviours apply on iPhone only.
- `.tabViewBottomAccessory { }` shows content just above the tab bar; its
  placement is described by `TabViewBottomAccessoryPlacement`.

The example mixes both groups, so as written it needs iOS 26.

## Pitfalls

- A view model per tab. Keep state local to views or in shared `@Observable`
  services.
- An `AppTab.id` that changes between launches; dynamic cases must hash on a
  stable identifier.
- Special tabs such as compose that change the selection.
- `.tabItem { }` instead of `Tab(value:)` with `TabView(selection:)`, which
  loses typed selection.
- Expecting `tabBarMinimizeBehavior` to work on iPad or Mac; it does nothing
  there.
