Double-Column Navigation Split View in SwiftUI


We wrote our iOS & iPadOS app Cleora - HTTP and WebSocket client in SwiftUI. One of the challenges we faced was implementing the Navigation Split View. Our goal was to have a two-column split view when in full-screen in both landscape and portrait orientations.


This is what the end result looks like.


iPad screenshot iPad screenshot


SwiftUI doesn’t have a direct equivalent for UIKit's UISplitViewController, this functionality is available using NavigationView instead. With this simplified example we will get a split view on iPad.


NavigationView {
    List(self.requestUrls, id: \.self) { url in
        NavigationLink(destination:
            RequestDetailView(url: url)
        ) {
            Text(url)
        }
    }
    .navigationBarTitle("Requests")
    
    Text("Nothing Selected.")
}


While it looks great in landscape, in portrait mode only the detail view will be shown initially and the list view will be available with the sliding panel on the left. This is not a perfect user experience on launch and it’s not immediately obvious that to access the list of all the requests you need to swipe from the left.

iPad screenshot iPad screenshot


We haven't found a way in SwiftUI to programatically slide the master view in and out, so we decided to force the view to always show 2 columns when in full screen in both portrait and landscape.


Update: As of iPadOS 13.4, there is now a button to slide the master view in and out, but this article is still relevant if you want to force the double -column display.


We were hoping to acheive this by setting the modifier navigationViewStyle to DoubleColumnNavigationViewStyle. From Apple documentation we can only know that this value exists but unfortunately, there is no description about how it is intended to work. Using this modifier didn't solve our problem.


So, after some more research and after trying various solutions, we decided to go with the one mentionned in this Stack Overflow discussion... add some padding to the NavigationView 🤷‍♀️


NavigationView {
    List(requestUrls, id: \.self) { url in
        NavigationLink(destination:
            RequestDetailView(url: url)
        ) {
            Text(url)
        }
    }
    .navigationBarTitle("Requests")
    
    Text("Nothing Selected.")
}
.padding(.leading, 1)


With that, the navigation view is now showing two-columns in portrait orientation. But in landscape, on smaller iPads, the view is only showing one column 🤯



In the end, we decided to add the padding depending on the orientation. We check if the height is bigger than the width and only then add the padding. We wrapped the NavigationView in a Geometry reader to get the size. Here is the final working version.


GeometryReader { geo in
    NavigationView {
        List(self.requestUrls, id: \.self) { url in
            NavigationLink(destination:
                RequestDetailView(url: url)
            ) {
                Text(url)
            }
        }
        .navigationBarTitle("Requests")
        
        Text("Nothing Selected.")
    }
    .padding(.leading, geo.size.height > geo.size.width ? 1 : 0)
}


This code works in Xcode Version 11.3.1 and iPadOS 13.3. You can find the full example here.