```html SwiftUI: How to Build iPad Split View (iOS 17+, 2026)

How to Build iPad Split View in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: NavigationSplitView Updated: May 11, 2026
TL;DR

Use NavigationSplitView to get a native iPad split-view layout — it automatically collapses to a stack on iPhone and adapts to Stage Manager and Slide Over without extra code.

struct ContentView: View {
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            List(Item.samples, selection: $selectedItem) { item in
                Text(item.title).tag(item)
            }
            .navigationTitle("Items")
        } detail: {
            if let item = selectedItem {
                DetailView(item: item)
            } else {
                Text("Select an item")
                    .foregroundStyle(.secondary)
            }
        }
    }
}

Full implementation

The example below builds a two-column split view — sidebar on the left, detail on the right — backed by an @Observable model. A NavigationSplitViewVisibility binding lets users and the app itself toggle the sidebar programmatically. Column widths are pinned with navigationSplitViewColumnWidth() so the sidebar never grows too wide on large iPads.

import SwiftUI

// MARK: - Model

struct Item: Identifiable, Hashable {
    let id = UUID()
    var title: String
    var body: String
    var systemImage: String

    static let samples: [Item] = [
        Item(title: "Inbox",    body: "All unread messages live here.",   systemImage: "tray"),
        Item(title: "Starred",  body: "Your favourited items appear here.", systemImage: "star"),
        Item(title: "Archive",  body: "Archived content is stored here.",  systemImage: "archivebox"),
        Item(title: "Settings", body: "App preferences and account info.", systemImage: "gear"),
    ]
}

// MARK: - Root

struct ContentView: View {
    @State private var columnVisibility: NavigationSplitViewVisibility = .all
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            // Sidebar column
            List(Item.samples, selection: $selectedItem) { item in
                Label(item.title, systemImage: item.systemImage)
                    .tag(item)
            }
            .navigationTitle("Library")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        columnVisibility = columnVisibility == .all
                            ? .detailOnly : .all
                    } label: {
                        Label("Toggle Sidebar",
                              systemImage: "sidebar.left")
                    }
                }
            }
            // Pin the sidebar to a comfortable fixed width
            .navigationSplitViewColumnWidth(min: 220, ideal: 260, max: 320)
        } detail: {
            if let item = selectedItem {
                DetailView(item: item)
            } else {
                ContentUnavailableView(
                    "Nothing Selected",
                    systemImage: "sidebar.left",
                    description: Text("Pick an item from the sidebar.")
                )
            }
        }
        // Prefer a side-by-side split rather than an overlay on iPad
        .navigationSplitViewStyle(.balanced)
    }
}

// MARK: - Detail

struct DetailView: View {
    let item: Item

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Label(item.title, systemImage: item.systemImage)
                    .font(.largeTitle.bold())
                Divider()
                Text(item.body)
                    .font(.body)
                    .foregroundStyle(.secondary)
            }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .navigationTitle(item.title)
        .navigationBarTitleDisplayMode(.inline)
    }
}

// MARK: - Preview

#Preview {
    ContentView()
}

How it works

  1. NavigationSplitView initializer — The two-closure form (sidebar: + detail:) gives you the classic mail-style layout. Pass the optional columnVisibility: binding if you need programmatic control; omit it for the system default.
  2. List selection bindingList(Item.samples, selection: $selectedItem) wires row taps directly to the state that drives the detail column. When selectedItem becomes non-nil the detail redraws automatically.
  3. navigationSplitViewColumnWidth() — Called on the sidebar List, this modifier gives the system a min/ideal/max range. The sidebar stays readable on a 13-inch iPad Pro without stretching awkwardly.
  4. navigationSplitViewStyle(.balanced) — Applied to the root split view, this style gives both columns proportional space. Use .prominentDetail if the detail should dominate, or .automatic to let the system decide per size class.
  5. ContentUnavailableView — The iOS 17 API replaces the old "empty state" pattern with a built-in component that handles layout, tinting, and Dark Mode for you when no item is selected.

Variants

Three-column layout (sidebar + content + detail)

Add a third closure for a Mail-style "mailbox → message list → reading pane" hierarchy. Each column gets its own selection binding.

struct ThreeColumnView: View {
    @State private var selectedFolder: Folder?
    @State private var selectedMessage: Message?

    var body: some View {
        NavigationSplitView {
            FolderList(selection: $selectedFolder)
                .navigationTitle("Folders")
        } content: {
            if let folder = selectedFolder {
                MessageList(folder: folder,
                            selection: $selectedMessage)
                    .navigationTitle(folder.name)
            } else {
                Text("Choose a folder").foregroundStyle(.secondary)
            }
        } detail: {
            if let message = selectedMessage {
                MessageDetail(message: message)
            } else {
                ContentUnavailableView("No Message",
                    systemImage: "envelope")
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

Hiding the sidebar by default on compact size classes

On iPhone the split view collapses to a NavigationStack automatically — you do not need an explicit @Environment(\.horizontalSizeClass) guard. If you want to start with the sidebar hidden on iPad too, initialise the visibility binding as @State private var columnVisibility: NavigationSplitViewVisibility = .detailOnly and the system will respect it on first launch.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement iPad split view in SwiftUI for iOS 17+.
Use NavigationSplitView with a two-column sidebar + detail layout.
Bind List(selection:) to an @State optional model item.
Apply navigationSplitViewColumnWidth(min:ideal:max:) to the sidebar.
Show ContentUnavailableView when nothing is selected.
Make it accessible (VoiceOver labels on toolbar buttons).
Add a #Preview with realistic sample data.

In the Soarias Build phase, paste this prompt directly into Claude Code alongside your SwiftData model so the generated split view is immediately wired to your app's real data layer.

Related

FAQ

Does this work on iOS 16?

NavigationSplitView itself is available from iOS 16, but this page's full implementation relies on ContentUnavailableView and the @Observable macro, both of which require iOS 17. If you must support iOS 16, replace ContentUnavailableView with a plain Text view and use ObservableObject instead of @Observable.

How do I deep-link into a specific detail view on launch?

Pre-populate the selection state before the view appears. If you receive a URL or a push notification payload, resolve it to an Item in your App entry point and pass a binding down, or set the value inside .onOpenURL. SwiftUI will route to the correct detail column as soon as the binding has a value.

What is the UIKit equivalent?

UISplitViewController with style: .doubleColumn or .tripleColumn is the UIKit counterpart. NavigationSplitView wraps this behaviour and adds declarative state-driven routing, automatic iPhone collapse, and Stage Manager awareness — use SwiftUI unless you are maintaining an existing UIKit codebase.

Last reviewed: 2026-05-11 by the Soarias team.

```