swiftswiftuiswiftui-navigationlinkswiftui-navigationviewswiftui-tabview

When using an iPad and you change the screen from compact to regular, how do you keep the current view from going to a default view?


//  BottomNavigationBarView.swift
//  TrexlerLibrary
//
//  Created by Emanuel Luna on 2/3/24.
//

import SwiftUI
import UserNotifications

enum ActiveView: String, Hashable {
    case home
    case search
    case settings
}


struct MainView: View {
    @Environment(\.horizontalSizeClass) var sizeClass
    @State private var activeView: ActiveView? = .home

var body: some View {
    Group {
        if sizeClass == .compact {
            iphoneTabView
        } else {
            ipadExpansiveView
        }
    }
    .onAppear {
        InfoForLibrary().requestNotificationPermission()
    }
}

private var iphoneTabView: some View {
    TabView(selection: $activeView) {
        NavigationView { HomeView() }
            .tabItem {
                Label("Home", systemImage: "house")
            }
            .tag(ActiveView.home)
        
        NavigationView { SearchView() }
            .tabItem {
                Label("Search", systemImage: "magnifyingglass")
            }
            .tag(ActiveView.search)
        
        NavigationView { SettingsView() }
            .tabItem {
                Label("Settings", systemImage: "gear")
            }
            .tag(ActiveView.settings)
    }
}

private var ipadExpansiveView: some View {
    
    NavigationView {
        List {
            NavigationLink(destination: HomeView(), tag: ActiveView.home, selection: $activeView) {
                Label("Home", systemImage: "house")
            }
            
            NavigationLink(destination: SearchView(), tag: ActiveView.search, selection: $activeView) {
                Label("Search", systemImage: "magnifyingglass")
            }
            
            NavigationLink(destination: SettingsView(), tag: ActiveView.settings, selection: $activeView) {
                Label("Settings", systemImage: "gear")
            }
        }
        .listStyle(SidebarListStyle())
        .navigationTitle("Menu")
        
        // Render the currently selected view
        currentView
    }
}

private var currentView: some View {
    switch activeView {
    case .home:
        return AnyView(HomeView())
    case .search:
        return AnyView(SearchView())
    case .settings:
        return AnyView(SettingsView())
    case .none:
        return AnyView(HomeView())
        }
    }
}

// Preview
#Preview {
    MainView().environmentObject(UserSession())
}

This is my main view. When I use my app, it typically opens the HomeView, but the moment I use my iPad with Split View and move the app to a compact size or regular size, the view I was currently on moves back to the HomeView.

How can I make my app stay at the view I was currently on while the size changes?


Solution

  • This behaviour is probably because the type of activeView is different from the types of the tags you have assigned to each tab. ActiveTab? vs ActiveTab. That said, the way you wrote ipadExpansiveView is also rather problematic.

    For iOS 17, you should migrate to NavigationStack and NavigationSplitView.

    First, put the title, image name, and the views for each page into the ActiveView enum. This makes it more convenient to get these properties from an ActiveView value. I also made it CaseIterable so we can loop through its cases with ForEach later on.

    enum ActiveView: String, Hashable, CaseIterable {
        case home
        case search
        case settings
        
        var displayName: String {
            switch self {
            case .home:
                "Home"
            case .search:
                "Search"
            case .settings:
                "Settings"
            }
        }
        
        var systemImageName: String {
            switch self {
            case .home:
                "house"
            case .search:
                "magnifyingglass"
            case .settings:
                "gear"
            }
        }
        
        @ViewBuilder
        var view: some View {
            switch self {
            case .home:
                Text("Home")
            case .search:
                Text("Search")
            case .settings:
                Text("Settings")
            }
        }
    }
    

    I would also make iPhoneTabView and iPadExpansiveViews separate View structs:

    struct iPhoneTabView: View {
        @Binding var activeView: ActiveView
        
        var body: some View {
            TabView(selection: $activeView) {
                ForEach(ActiveView.allCases, id: \.self) { tab in
                    NavigationStack { tab.view }
                        .tabItem {
                            Label(tab.displayName, systemImage: tab.systemImageName)
                        }
                }
            }
        }
    }
    
    struct iPadExpansiveView: View {
        @Binding var activeView: ActiveView
        
        var body: some View {
            NavigationSplitView {
                List(ActiveView.allCases, id: \.self, selection: Binding($activeView)) { tab in
                    NavigationLink(value: tab) {
                        Label(tab.displayName, systemImage: tab.systemImageName)
                    }
                }
            } detail: {
                activeView.view
            }
    
        }
    }
    

    The NavigationSplitView is driven by the selection of List. The selection: parameter is expecting a Binding<ActiveView?>, but it doesn't make sense for there to be no active view, does it? Therefore, iPadExpansiveView takes a non-optional ActiveView binding, and converts it to Binding<ActiveView?> just before passing it to selection:.

    MainView would then look like this, with a non-optional activeView state.

    @Environment(\.horizontalSizeClass) var sizeClass
    @State private var activeView: ActiveView = .home
    
    var body: some View {
        Group {
            if sizeClass == .compact {
                iPhoneTabView(activeView: $activeView)
            } else {
                iPadExpansiveView(activeView: $activeView)
            }
        }
    }