How do I remove the sidebar toggle from a NavigationSplitView on iPad only in landscape mode

402 Views Asked by At

How do I remove the sidebar toggle from a NavigationSplitView on iPad in landscape mode but keep it in portrait mode? Similar to how it works in the home app in iOS 17.

I have this so far but it doesn't work correctly at first launch before switching orientation once:

struct SidebarMainView: View {
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    @State private var isPortrait = true

    var body: some View {
        NavigationSplitView {
            List {
                Text("1")
                Text("2")
                Text("3")
            }
            .listStyle(SidebarListStyle()).toolbar(removing: isPortrait ? .none : .sidebarToggle).navigationTitle("HP").onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                guard let scene = UIApplication.shared.windows.first?.windowScene else { return }
                self.isPortrait = scene.interfaceOrientation.isPortrait
            }
        } detail: {
            Text("Select a department: \(self.isPortrait ? "yes" : "no")")
        }
    }
}

For reference, this is what the Home app looks like in different layouts. Note the forced and optional sidebar in each layout. I want to accomplish the same.

enter image description here enter image description here enter image description here enter image description here enter image description here

3

There are 3 best solutions below

6
Nayan Dave On BEST ANSWER

For iOS 17+
We can use UIDevice.orientationDidChangeNotification to detect current orientation and toolbar(removing:) for removing sidebar toggle button.

struct ContentView: View {
    @State private var deviceOrientation = UIDevice.current.orientation

    var body: some View {
        NavigationSplitView {
            Text("sidebar")
                .detectOrientation($deviceOrientation)
                // After removing sidebar toggle, it was not adding when refreshing (Instead Visibility solves the problem)
                // .toolbar(removing:  isOrientationPortrait() ? .none : .sidebarToggle)
                .toolbar(isOrientationPortrait() ? .visible : .hidden, for: .navigationBar)
        } detail: {
            Text("Detail")
        }
    }

    private func isOrientationPortrait() -> Bool {
        return deviceOrientation == .portrait || deviceOrientation == .portraitUpsideDown
    }
}


extension View {
    func detectOrientation(_ orientation: Binding<UIDeviceOrientation>) -> some View {
        modifier(DetectOrientation(orientation: orientation))
    }
}

struct DetectOrientation: ViewModifier {
    
    @Binding var orientation: UIDeviceOrientation
    
    func body(content: Content) -> some View {
        content.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                orientation = UIDevice.current.orientation
            }
    }
}

Hope It Helps !

1
user16930239 On

if windowWidth < WindowHieght then use navigation Tab Bar else use side menu navigation

it is a s simple as that

0
Benzy Neez On

As I wrote in a comment, the basic logic for this layout is quite easy to implement:

  • The environment variable horizontalSizeClass can be examined at first launch. This tells you if the horizontal size is regular or compact. However, it doesn't tell you anything about the device orientation.
  • To determine whether the height is greater than the width, a GeometryReader can be used. This also works at launch and it reports the dimensions of split screen reliably.

However, I found that getting it to work as you described was not quite so straightforward as expected, for three reasons:

  1. You said you wanted to switch to a TabView for compact layout. This does not come for free with NavigationSplitView.

-> This requires separate implementation.

  1. I found that if launched in landscape orientation, the sidebar toggle would be removed and would never re-appear, even when switching to portrait orientation.

-> To resolve this, an elaborate if-else switch can be used to force the split view to be rebuilt when switching between portrait and landscape.

  1. When two-thirds split mode is used in landscape orientation, the height may be smaller than the width on some devices (especially if safe-area insets are not ignored). Technically, this is landscape orientation, but you showed in your screenshot that you want the sidebar toggle to be visible.

-> To resolve this, the height:width ratio can be compared to a threshold of, say, 0.9.

So here's my attempt to get it working. Hope it helps:

enum NavDestination: Int, CaseIterable, Identifiable {
    case one = 1
    case two = 2
    case three = 3

    var id: Int {
        self.rawValue
    }

    var title: String {
        "\(self)".capitalized
    }

    var iconName: String {
        "\(self.rawValue).circle"
    }
}

struct SidebarMainView: View {
    @State private var selection: NavDestination? = .one
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    let minHeightWidthRatioPortrait = 0.9

    private func detailView(for navDestination: NavDestination) -> some View {
        Text(navDestination.title)
            .font(.largeTitle)
    }

    private var tabView: some View {
        TabView(selection: $selection) {
            ForEach(NavDestination.allCases) { dest in
                detailView(for: dest)
                    .tabItem {
                        Label(dest.title, systemImage: dest.iconName)
                    }
                    .tag(dest)
            }
        }
    }

    private func splitView(isPortrait: Bool) -> some View {
        NavigationSplitView {
            List(NavDestination.allCases, selection: $selection) { dest in
                Label(dest.title, systemImage: dest.iconName)
                    .tag(dest)
            }
            .listStyle(.sidebar)
            .toolbar(removing: isPortrait ? .none : .sidebarToggle)
            .navigationTitle("HP")
        } detail: {
            if let selection {
                detailView(for: selection)
            } else {
                Text("Please select an option")
            }
        }
    }

    var body: some View {
        if horizontalSizeClass == .compact {
            tabView
        } else {
            GeometryReader { proxy in
                let w = proxy.size.width
                let h = proxy.size.height
                let heightWidthRatio = h / max(1, w)
                let isPortrait = heightWidthRatio >= minHeightWidthRatioPortrait

                // An elaborate if-else is necessary in order to force
                // the sidebar toggle to be shown when changing to
                // portrait orientation, for the case of when the
                // view is initially launched in landscape orientation
                // (with sidebar hidden). Using a single function call
                // and passing the flag as parameter is not sufficient
                if isPortrait {
                    splitView(isPortrait: true)
                } else {
                    splitView(isPortrait: false)
                }
            }
        }
    }
}

The screenshots are from an iPad mini.

Screenshots