NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Making NavigationView work in landscape

Paul Hudson    @twostraws   

When we use a NavigationView, by default SwiftUI expects us to provide both a primary view and a secondary detail view that can be shown side by side, with the primary view shown on the left and the secondary on the right. Previously we solved this by using StackNavigationViewStyle() as the navigation style for our NavigationView, which tells SwiftUI we only want to show one view, but here we actually want the two-view behavior so we aren’t going to use that.

On landscape iPhones that are big enough – iPhone 11 Pro Max, for example – SwiftUI’s default behavior is to show the secondary view, and provide the primary view as a slide over. It’s always been there, but you might not have realized until now: try sliding from the left edge of your screen to reveal the ContentView we just made. If you tap rows in there you’ll see the text behind ContentView change as the result of our NavigationLink, and if you tap on the text behind you can dismiss the ContentView slide over.

Now, there is a problem here, and it’s the same problem you’ve had all along: it’s not immediately obvious to the user that they need to slide from the left to reveal the list of options. In UIKit this can be fixed easily, but SwiftUI doesn’t give us an alternative right now so we’re going to work around the problem: we’ll create a second view to show on the right by default, and use that to help the user discover the left-hand list.

First, create a new SwiftUI view called WelcomeView, then give it this code:

struct WelcomeView: View {
    var body: some View {
        VStack {
            Text("Welcome to SnowSeeker!")
                .font(.largeTitle)

            Text("Please select a resort from the left-hand menu; swipe from the left edge to show it.")
                .foregroundColor(.secondary)
        }
    }
}

That’s all just static text; it will only be shown when the app first launches, because as soon as the user taps any of our navigation links it will get replaced with whatever they were navigating to.

To put that into ContentView so the two parts of our UI can be used side by side, all we need to do is add a second view to our NavigationView like this:

NavigationView {
    List(resorts) { resort in
        // all the previous list code
    }
    .navigationBarTitle("Resorts")

    WelcomeView()
}

That’s enough for SwiftUI to understand exactly what we want. Try running the app on several different devices, both in portrait and landscape, to see how SwiftUI responds:

  • On an iPhone 11 Pro you’ll see ContentView in both portrait and landscape.
  • On an iPhone 11 (not 11 Pro – 11 Amateur?!) you’ll see ContentView in portrait and WelcomeView in landscape.
  • On an iPad you’ll also see ContentView in portrait and WelcomeView in landscape.

The first two of those might seem backwards, but it’s a result of Apple’s slightly odd hardware choices: although the iPhone 11 Pro uses a Super Retina display at 3x resolution it is physically smaller than the iPhone 11’s 2x display, so Apple considers it too small to support the slide over ContentView.

Although UIKit lets us control whether the primary view should be shown on iPad portrait, this is not yet possible in SwiftUI. However, we can stop the iPhone 11 from using the slide over approach if that’s what you want – try it first and see what you think. If you want it gone, add this extension to your project:

extension View {
    func phoneOnlyStackNavigationView() -> some View {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
        } else {
            return AnyView(self)
        }
    }
}

That uses Apple’s UIDevice class to detect whether we are currently running on a phone or a tablet, and if it’s a phone enables the simpler StackNavigationViewStyle approach. We need to use type erasure here because the two returned view types are different.

Once you have that extension, simply add the .phoneOnlyStackNavigationView() modifier to your NavigationView so that iPads retain their default behavior whilst iPhones always use stack navigation.

Again, give it a try and see what you think – it’s your app, and it’s important you like how it works.

Tip: I’m not going to be using this modifier in my own project because I prefer to use Apple’s default behavior where possible, but don’t let that stop you from making your own choice!

Hacking with Swift is sponsored by NSSpain

SPONSORED Announcing NSSpain 2020: Remote Edition! An online, continuous conference for iOS developers. We’ll start on Thursday and finish on Friday, with talks, activities, and lots of fun for 36 hours, non-stop. Sound good? Join us!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

Link copied to your pasteboard.