Swiftyn LogoSwiftyn
LearnInterview PrepRoadmapsArchitect Profile
Swift LanguageSwiftUIUIKitiOS ConceptsmacOS

SwiftUI Topics

Introduction to SwiftUISwiftUI App LifecycleViews and ModifiersTextImageShapesSF SymbolsButtonsStacksSpacerDividerScrollViewGroupsSectionsHStackVStackZStackLazyVStackLazyHStackLazyVGridLazyHGridGridGridRowGeometryReaderSafeAreaFrames and AlignmentPaddingOverlay and BackgroundPreferenceKeyAnchor PreferencesCustom Layouts@State@Binding@ObservedObject@StateObject@EnvironmentObject@Environment@PublishedObservableObject@Observable@BindableData Flow in SwiftUITwo Way BindingNavigationStackNavigationSplitViewNavigationPathProgrammatic NavigationTabViewDeep LinkingSheetFullScreenCoverPopoverAlertsCustom AnimationsAsync Await in SwiftUIUIKit Integration
Browse SwiftUI Topics
Introduction to SwiftUISwiftUI App LifecycleViews and ModifiersTextImageShapesSF SymbolsButtonsStacksSpacerDividerScrollViewGroupsSectionsHStackVStackZStackLazyVStackLazyHStackLazyVGridLazyHGridGridGridRowGeometryReaderSafeAreaFrames and AlignmentPaddingOverlay and BackgroundPreferenceKeyAnchor PreferencesCustom Layouts@State@Binding@ObservedObject@StateObject@EnvironmentObject@Environment@PublishedObservableObject@Observable@BindableData Flow in SwiftUITwo Way BindingNavigationStackNavigationSplitViewNavigationPathProgrammatic NavigationTabViewDeep LinkingSheetFullScreenCoverPopoverAlertsCustom AnimationsAsync Await in SwiftUIUIKit Integration
Swiftyn Logo

Swiftyn

The go-to platform for Apple developers. Swift, SwiftUI, and beyond.

Questions? Email us at support@swe180.com

Categories

  • SwiftUI
  • Swift Language
  • Xcode
  • visionOS

Our Products

  • SWE180
  • One Percent Engineer

Resources

  • About
  • RSS Feed
  • Apple Developer

© 2026 Swiftyn. All rights reserved.

Privacy PolicyTerms of Service

Swiftyn is the premier learning platform and developer resource for mastering the Apple ecosystem. Whether you are an aspiring iOS developer looking to learn Swift 6, a macOS engineer diving into advanced system architecture, or an XR pioneer building the future with visionOS, our beautifully crafted tutorials, roadmaps, and interview prep guides have you covered. Built by Apple developers, for Apple developers.

SwiftUI12 min read

Mastering SwiftUI's NavigationSplitView for Adaptable iOS Apps

SwiftUI's NavigationSplitView is a powerful container view introduced in iOS 16, iPadOS 16, and macOS 13. It enables you to build adaptable, multi-column navigation experiences that adjust gracefully across different screen sizes and orientations. This guide will walk you through implementing and optimising NavigationSplitView for your applications.

Understanding NavigationSplitView

Before SwiftUI 4 (iOS 16, macOS 13), building adaptable multi-column layouts, especially for navigation, often involved complex conditional views and geometry readers. While NavigationView (now NavigationStack and NavigationSplitView) provided basic hierarchy, it lacked native support for the dynamic, multi-pane structures common on larger screens like iPads and Macs.

NavigationSplitView changes this by offering a declarative way to create a master-detail or even a three-column interface. It automatically manages the visibility and behaviour of its columns based on the available screen real estate and the current DisplayMode.

At its core, NavigationSplitView is designed to show two or three distinct content panes: a sidebar, a content list (often called primary), and an optional detail pane (secondary). Think of it like the Mail app on iPad or Mac, where you have a list of mailboxes, a list of emails within a selected mailbox, and then the content of a selected email.

It's important to differentiate NavigationSplitView from NavigationStack. NavigationStack replaces the traditional NavigationView for linear navigation flows (pushing and popping views). NavigationSplitView, on the other hand, is about simultaneously presenting multiple hierarchical views side-by-side, which can contain NavigationStack instances within their panes. You would typically use NavigationSplitView at the root of your application or a major section to define the overall layout, and then use NavigationStack within its panes for deeper navigation within a specific column.

Compatibility Note: NavigationSplitView is available for iOS 16+, iPadOS 16+, macOS 13+, tvOS 16+, and watchOS 9+. Ensure your project's deployment target meets these requirements.

Basic Two-Column Implementation

The simplest form of NavigationSplitView provides a two-column layout: a sidebar and a detail view. This is ideal for scenarios where you have a list of items and you want to display the details of the selected item.

To create a NavigationSplitView, you use two closures: one for the sidebar and one for the detail view. The NavigationSplitView automatically handles the presentation logic. On compact environments (like iPhone portrait), the sidebar will typically be presented first, allowing you to navigate to the detail view by selection. On regular environments (like iPad landscape or macOS), both will be visible simultaneously.

Let's start with a simple example where we display a list of fruits in the sidebar and show details for the selected fruit.

swift
import SwiftUI

struct Fruit: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let color: String
}

struct TwoColumnSplitView: View {
    @State private var fruits = [
        Fruit(name: "Apple", color: "Red"),
        Fruit(name: "Banana", color: "Yellow"),
        Fruit(name: "Orange", color: "Orange"),
        Fruit(name: "Grape", color: "Purple")
    ]
    @State private var selectedFruit: Fruit?

    var body: some View {
        NavigationSplitView {
            List(fruits, selection: $selectedFruit) {
                Text($0.name)
            }
            .navigationTitle("Fruits")
        } detail: {
            if let selectedFruit = selectedFruit {
                FruitDetailView(fruit: selectedFruit)
            } else {
                Text("Select a fruit")
            }
        }
    }
}

struct FruitDetailView: View {
    let fruit: Fruit

    var body: some View {
        VStack {
            Text(fruit.name)
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding()
            Text("Color: \(fruit.color)")
                .font(.title2)
        }
        .navigationTitle(fruit.name)
    }
}

struct TwoColumnSplitView_Previews: PreviewProvider {
    static var previews: some View {
        TwoColumnSplitView()
            .previewDevice(PreviewDevice(rawValue: "iPad Pro (11-inch)"))
            .previewDisplayName("iPad Pro")
        TwoColumnSplitView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 14 Pro"))
            .previewDisplayName("iPhone 14 Pro")
    }
}

Implementing a Three-Column Layout

For more complex navigation hierarchies, like a full email client or a file browser, NavigationSplitView supports a three-column layout: a sidebar, a content (primary) view, and a detail (secondary) view.

This is achieved by providing three distinct closures to the NavigationSplitView initializer: sidebar, content, and detail. The structure remains highly flexible, allowing you to embed NavigationStack instances within each column for independent navigation paths. For example, you might have categories in the sidebar, items within a selected category in the content view, and the details of a selected item in the detail view.

Consider extending our fruit example to include fruit categories. When a category is selected in the sidebar, a list of fruits for that category appears in the content column, and selecting a fruit displays its details in the third column.

swift
import SwiftUI

struct Category: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let fruits: [Fruit]
}

struct ThreeColumnSplitView: View {
    @State private var categories: [Category] = [
        Category(name: "Berries", fruits: [
            Fruit(name: "Strawberry", color: "Red"),
            Fruit(name: "Blueberry", color: "Blue")
        ]),
        Category(name: "Citrus", fruits: [
            Fruit(name: "Lemon", color: "Yellow"),
            Fruit(name: "Lime", color: "Green")
        ])
    ]
    @State private var selectedCategory: Category?
    @State private var selectedFruit: Fruit?

    var body: some View {
        NavigationSplitView {
            List(categories, selection: $selectedCategory) {
                Text($0.name)
            }
            .navigationTitle("Categories")
        } content: {
            if let selectedCategory = selectedCategory {
                List(selectedCategory.fruits, selection: $selectedFruit) {
                    Text($0.name)
                }
                .navigationTitle(selectedCategory.name)
            } else {
                Text("Select a category")
            }
        } detail: {
            if let selectedFruit = selectedFruit {
                FruitDetailView(fruit: selectedFruit)
            } else {
                Text("Select a fruit")
            }
        }
    }
}

struct ThreeColumnSplitView_Previews: PreviewProvider {
    static var previews: some View {
        ThreeColumnSplitView()
            .previewDevice(PreviewDevice(rawValue: "iPad Pro (11-inch)"))
            .previewDisplayName("iPad Pro")
        ThreeColumnSplitView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 14 Pro"))
            .previewDisplayName("iPhone 14 Pro")
    }
}

Controlling Column Visibility and Display Mode

NavigationSplitView provides powerful modifiers to fine-tune its behavior. The navigationSplitViewColumnWidth(_:) modifier allows you to suggest a preferred width for your sidebar and content columns. While the system may adjust this based on available space, it's a good way to set initial proportions.

More critically, navigationSplitViewStyle(_:) helps you define the preferred display mode. You can choose from:

  • .automatic: The system decides the best display mode based on device size and orientation (e.g., collapsed to one column on iPhone, two or three on iPad).
  • .forUncollapsedColumns: Always display all columns that fit, never collapsing fully unless forced by space constraints.
  • .balanced: A good default for three-column layouts, aiming to balance visibility and available space.
  • .prominentDetail: Prioritizes the detail column, making it larger and potentially collapsing others.

You can also dynamically control the visibility of columns using @State variables and the navigationSplitViewColumnBehavior(_:) modifier. This is particularly useful for advanced scenarios where you might want to programmatically hide or show specific columns based on user actions or application state.

Let's add some customisation to our previous example using these modifiers.

swift
import SwiftUI

struct CustomisedSplitView: View {
    @State private var categories: [Category] = [
        Category(name: "Berries", fruits: [
            Fruit(name: "Strawberry", color: "Red"),
            Fruit(name: "Blueberry", color: "Blue")
        ]),
        Category(name: "Citrus", fruits: [
            Fruit(name: "Lemon", color: "Yellow"),
            Fruit(name: "Lime", color: "Green")
        ])
    ]
    @State private var selectedCategory: Category?
    @State private var selectedFruit: Fruit?
    @State private var columnVisibility: NavigationSplitViewVisibility = .automatic

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            List(categories, selection: $selectedCategory) {
                Text($0.name)
            }
            .navigationTitle("Categories")
            .navigationSplitViewColumnWidth(180)
        } content: {
            if let selectedCategory = selectedCategory {
                List(selectedCategory.fruits, selection: $selectedFruit) {
                    Text($0.name)
                }
                .navigationTitle(selectedCategory.name)
            } else {
                Text("Select a category")
            }
            .navigationSplitViewColumnWidth(min: 200, ideal: 250)
        } detail: {
            if let selectedFruit = selectedFruit {
                FruitDetailView(fruit: selectedFruit)
            } else {
                Text("Select a fruit")
            }
        }
        .navigationSplitViewStyle(.forUncollapsedColumns)
        // You can also change visibility dynamically:
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Button("Toggle Sidebar") {
                    columnVisibility = (columnVisibility == .all ? .detailOnly : .all)
                }
                .keyboardShortcut("\\", modifiers: .command)
            }
        }
    }
}

struct CustomisedSplitView_Previews: PreviewProvider {
    static var previews: some View {
        CustomisedSplitView().previewDevice(PreviewDevice(rawValue: "iPad Pro (11-inch)"))
    }
}

Styling and Appearance Customisation

While NavigationSplitView handles many adaptable UI concerns automatically, you can still apply standard SwiftUI view modifiers to customise the appearance of its individual columns. For instance, you can use .background(), .padding(), or custom styling modifiers within each sidebar, content, and detail closure.

Remember that each pane of a NavigationSplitView is a distinct view hierarchy. This means you can embed other sophisticated SwiftUI views, including NavigationStack, Toolbar, and custom components, within each column to create rich user interfaces. The system handles the transitions and visual integration during column changes, ensuring a consistent user experience.

For example, you could add custom toolbars to each column, or specific background colours to visually differentiate them, enhancing user navigation and readability across complex applications. Just ensure that when adding navigation items, you use the .toolbar modifier within a view that itself is inside a NavigationStack or NavigationSplitView column.

Best Practices for NavigationSplitView

To build robust and user-friendly applications with NavigationSplitView, consider these best practices:

  1. Use NavigationStack within panes: For deep, linear navigation within a specific column (e.g., navigating from a list of emails to a conversation thread), embed a NavigationStack inside your sidebar, content, or detail views. This creates clear navigation paths within each pane.
  2. State Management: Use @State and @Binding to manage the selected items across columns. The selection state in your sidebar should drive the content of your primary view, and the primary view's selection should drive the detail view. This ensures data consistency.
  3. Handle Empty States: Always provide a clear placeholder or instructional text when no item is selected in a column that expects one (e.g., 'Select an item' in the detail view). This improves the user experience, especially on first launch or when all items are deselected.
  4. Accessibility: Ensure all interactive elements within your NavigationSplitView are accessible. Use appropriate accessibility labels and traits to provide a good experience for users with disabilities.
  5. Test on all Devices: Thoroughly test your NavigationSplitView on different devices (iPhone, iPad, Mac) and orientations to ensure it adapts gracefully. Use SwiftUI Previews with previewDevice to quickly emulate various environments.
  6. Avoid Over-nesting: While powerful, deep nesting of NavigationSplitView instances can become confusing. If your hierarchy is too complex, consider if a different UI pattern might be more appropriate or if you can simplify your data model.
  7. Consider NavigationSplitView at the Root: For applications designed around a primary-detail flow, placing NavigationSplitView at the very root of your app's WindowGroup is often the most straightforward approach. This ensures it manages the top-level navigation structure.

Rigid Layouts

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Rigid Layouts

Before iOS 16, creating adaptive multi-column layouts across iPhone, iPad, and Mac often required extensive conditional views, `GeometryReader`, and platform-specific code. This led to complex, hard-to-maintain UIs that didn't gracefully handle orientation changes or multitasking modes, resulting in a fractured user experience.

swift
if UIDevice.current.userInterfaceIdiom == .pad {
    HStack { Sidebar(); DetailView() }
} else {
    NavigationView { Sidebar() }
}

TASK HIERARCHY: NavigationSplitView Adaptation

NavigationSplitView dynamically manages column visibility and presentation based on its initializer (`sidebar`, `detail` vs. `sidebar`, `content`, `detail`) and the current display environment's size classes and available space. It determines which columns to show or hide, and how to transition between states (e.g., pushing the detail view onto a stack on compact widths).

NavigationSplitView
Sidebar Column
Content Column (Optional)
Detail Column
1

1. Environment Assessment

System evaluates current device, orientation, and multitasking state (compact vs. regular horizontal size class).

2

2. Preferred Display Mode

NavigationSplitView considers its `.navigationSplitViewStyle()` (e.g., `.automatic`, `.forUncollapsedColumns`).

3

3. Column Visibility

Decides which columns (sidebar, content, detail) are currently visible simultaneously.

4

4. Layout & Transitions

Renders visible columns side-by-side or stacks them on top of each other, handling user-initiated swipes or system buttons for navigation between stacked columns.

Visualized execution hierarchy.

Powerful Guarantees

Automatic Adaptation

Maintains layout integrity and user experience across diverse screen sizes (iPhone, iPad, Mac) and orientations without manual `if #available` checks.

System-Managed Transitions

Handles animations and gestures for collapsing/expanding columns, ensuring a fluid and native feel.

Consistent User Interaction

Provides intuitive navigation patterns (e.g., swipe to reveal sidebar) that users expect on Apple platforms.

REAL PRODUCTION EXAMPLE: A Mail Client

A complex email client needs to display mailboxes, a list of emails in a selected mailbox, and the content of a selected email. On iPhone, these appear as distinct screens in a stack. On iPad landscape or Mac, they appear side-by-side. Before, this required immense conditional logic and view modifications.

Impact / Results
Seamless multi-device experience
Reduced UI code complexity
Improved user productivity on large screens
THE FIX or SOLUTION
swift
struct MailApp: View {
    @State private var selectedMailbox: Mailbox?
    @State private var selectedEmail: Email?

    var body: some View {
        NavigationSplitView {
            MailboxListView(selection: $selectedMailbox)
        } content: {
            if let mailbox = selectedMailbox {
                EmailListView(mailbox: mailbox, selection: $selectedEmail)
            } else {
                Text("Select a Mailbox")
            }
        } detail: {
            if let email = selectedEmail {
                EmailContentView(email: email)
            } else {
                Text("Select an Email")
            }
        }
        .navigationSplitViewStyle(.automatic)
    }
}

INTERVIEW PERSPECTIVE

Common Question

“Explain how `NavigationSplitView` helps build adaptive UIs compared to previous SwiftUI versions.”

Strong Answer

`NavigationSplitView` provides a declarative, multi-column container that automatically adapts its layout based on the current environment's size classes and display mode. It replaces the need for manual `GeometryReader` checks and conditional views, offering a robust, system-managed solution for master-detail or three-pane interfaces on iOS 16+, iPadOS 16+, and macOS 13+. This significantly simplifies building UIs that feel native across all Apple devices.

Interviewers Expect you to understand:
  • Declarative syntax
  • Automatic adaptation
  • Replaces manual conditional logic
  • Supports multiple columns (sidebar, content, detail)
  • Available from iOS 16+
KEY TAKEAWAY

Embrace NavigationSplitView for adaptable and consistent multi-column navigation across iOS, iPadOS, and macOS, drastically simplifying complex adaptive UI development in SwiftUI.

Common Interview Questions

When should I use `NavigationSplitView` versus `NavigationStack`?

`NavigationSplitView` is for displaying multiple hierarchical content panes side-by-side, adapting to screen size (e.g., master-detail on iPad). `NavigationStack` is for linear navigation, pushing and popping views within a single column (e.g., drilling down into content on iPhone).

How do I make the sidebar in `NavigationSplitView` collapsible on iPad?

By default, `NavigationSplitView` handles collapsibility automatically based on device size using `.automatic` style. On iPad, the sidebar remains visible in landscape unless you explicitly set `navigationSplitViewStyle(.detailOnly)` or similar, or control `columnVisibility` programmatically. You can also define preferred column widths to influence its behavior.

Can I have 'push' navigation within a `NavigationSplitView` pane?

Yes, absolutely! You should embed a `NavigationStack` within each pane (sidebar, content, or detail) where you need linear push/pop navigation. For example, `NavigationSplitView { NavigationStack { SidebarContent() } } detail: { NavigationStack { DetailContent() } }`.

How do I pass data between the different columns of a `NavigationSplitView`?

You typically pass data using `@State` variables and `@Binding`. A common pattern is to declare `@State var selectedItem: Item?` in the parent `NavigationSplitView` and bind it to the `List` in your sidebar and pass it to the `DetailView`.

What's the difference between two-column and three-column `NavigationSplitView`?

A two-column `NavigationSplitView` has a `sidebar` and a `detail` view, often used for a list-detail pattern. A three-column `NavigationSplitView` adds a `content` (primary) view between the `sidebar` and `detail`, allowing for a more granular hierarchy (e.g., categories -> items -> item details).

#SwiftUI#NavigationSplitView#iOS 16#iPadOS#macOS#Adaptable UI