BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

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. This isn’t required – you can force the push/pop NavigationLink behavior if you want by using the navigationViewStyle() modifier – but in this project we actually want the two-view behavior so we aren’t going to use that.

On landscape iPhones that are big enough – iPhone 13 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 recently: 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
    }
    .navigationTitle("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 13 Pro you’ll see ContentView in both portrait and landscape, but on an iPhone 13 Pro Max you’ll see ContentView in portrait and WelcomeView in landscape. If you’re using an iPad, you might see several different things depending on the device orientation and whether the app has all the screen to itself as opposed to using split screen.

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 iPhones 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 {
    @ViewBuilder func phoneOnlyStackNavigationView() -> some View {
        if UIDevice.current.userInterfaceIdiom == .phone {
            self.navigationViewStyle(.stack)
        } else {
            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 the @ViewBuilder attribute 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 RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

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

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.