SwiftUI uses navigation to move between screens, and the method you choose depends on whether you want the user to go back or stay on one path
When you open an app and tap a button to see a detail page, or swipe back to the list, that movement between screens is called navigation. SwiftUI gives you two main tools to do this: NavigationStack (the newer way, available in iOS 16 and later) and NavigationLink (the older way that still works everywhere). The difference matters because NavigationStack lets users go back and forth, while NavigationLink can trap them on a one-way path if you're not careful.
The simplest case is a single tap that shows a new screen. You wrap a button or other tappable element in a NavigationLink, give it a destination (the screen you want to show), and SwiftUI handles the rest. But if you need to show multiple screens in sequence — like a list, then a detail, then an edit form — NavigationStack is cleaner because it keeps track of the path automatically.
Key Takeaways
- NavigationStack (iOS 16+) is the modern way to move between screens and automatically handles the back button for you.
- NavigationLink works on older iOS versions and is useful for single taps that show one new screen, but requires more setup if you want multiple screens in a row.
- The destination of a NavigationLink or NavigationStack must be a View — a SwiftUI component that describes what the screen looks like.
- You can pass data between screens by storing it in a variable and reading it on the destination screen, or by using @State to keep data in sync.
- Sheet and fullScreenCover are alternatives when you want a screen that slides up from the bottom or covers the whole screen, rather than pushing from the side.
NavigationStack: the modern approach for moving between screens
NavigationStack is the recommended way to handle navigation in SwiftUI if your app targets iOS 16 or later. You wrap your content in a NavigationStack, then use NavigationLink inside to move to new screens. The stack automatically shows a back button, so users can return to the previous screen without extra code from you.
Here's the basic structure: you create a NavigationStack at the top level of your view, then place NavigationLink buttons inside. Each NavigationLink points to a destination view. When the user taps the link, SwiftUI pushes the destination onto the stack and shows it. When they tap back, SwiftUI pops it off and returns to the previous screen.
The advantage is that NavigationStack tracks the entire path — if you have a list, then a detail, then an edit screen, the stack remembers all three. You can also use the @State variable to control which screen shows, which is useful if you want to navigate programmatically (for example, after a button press that saves data).
NavigationLink: the older method that works on all iOS versions
If your app needs to support iOS 15 or earlier, NavigationLink is your only choice. It works similarly to NavigationStack but requires you to wrap your entire view hierarchy in a NavigationView first, and you don't get automatic back button handling in all cases.
NavigationLink takes two parameters: a label (what the user sees and taps) and a destination (the screen to show). The label can be text, an image, or any other view. The destination is another view that describes the screen you want to show.
One common mistake with NavigationLink is creating a "dead end" — a screen the user can't navigate away from. This happens when you nest NavigationLinks incorrectly or forget to include a way back. NavigationStack avoids this because it manages the back button for you, but with NavigationLink you have to be deliberate about the structure.
Passing data between screens
Most of the time, the screen you navigate to needs information from the screen you came from. If you're showing a detail page for a product, you need to pass the product ID or the product object itself. SwiftUI handles this by letting you store data in a variable and read it on the destination screen.
The simplest approach is to define a property on the destination view that holds the data you need. For example, if you have a ProductDetail view, you give it a property called productId. When you create the NavigationLink, you pass the product ID to that property. SwiftUI then shows the ProductDetail screen with the correct product.
If the data needs to change (for example, the user edits the product name), you use @State to mark it as changeable. This tells SwiftUI to watch that variable and update the screen whenever it changes. You can also use @Binding to create a two-way connection, so changes on the detail screen automatically update the list screen.
Sheet and fullScreenCover: alternatives to pushing screens
Not every new screen should slide in from the side. Sometimes you want a screen that slides up from the bottom (a sheet) or covers the entire screen (fullScreenCover). SwiftUI provides both, and they work differently from NavigationStack and NavigationLink.
A sheet is useful for forms, settings, or anything the user might want to dismiss quickly. You attach it to a button or other view using the .sheet() modifier. When the user taps the button, the sheet slides up. When they swipe down or tap outside, it dismisses. A fullScreenCover is similar but takes up the entire screen and doesn't have a dismiss gesture — you have to provide a close button.
The key difference from navigation is that sheets and fullScreenCover don't add to a stack. They appear on top of the current screen, and when they close, you're back where you started. This is useful when you don't want the user to navigate deeper into a hierarchy — you want them to complete a task and return.
Combining navigation with state to control which screen shows
Sometimes you want to navigate based on what happened in your code, not just what the user tapped. For example, after the user logs in, you want to automatically show the home screen. You do this by using @State to track which screen should be visible, then checking that state in your NavigationStack or NavigationLink.
With NavigationStack, you can use the navigationDestination() modifier to say "if this condition is true, show this screen." This is more flexible than hardcoding a NavigationLink because you can change which screen shows based on any data in your app.
For example, you might have a @State variable called isLoggedIn. In your main view, you check this variable and show either a login screen or a home screen. When the user logs in, you set isLoggedIn to true, and SwiftUI automatically switches to the home screen. This pattern is cleaner than trying to navigate with NavigationLink because the navigation logic lives in one place.
Common mistakes and how to avoid them
The most common mistake is nesting NavigationLinks inside each other without a clear path back. This creates confusion because the user doesn't know how many times they need to tap back, or they get stuck on a screen with no way to return. To avoid this, keep your navigation structure flat — one NavigationStack or NavigationView at the top, with NavigationLinks inside, not nested NavigationLinks inside NavigationLinks.
Another mistake is passing too much data between screens. If you're passing an entire object, and the user edits it on the detail screen, you need to make sure the change gets back to the list screen. This is where @Binding comes in — it creates a two-way connection so both screens see the same data. Without it, the user might edit something, return to the list, and see the old data.
A third mistake is using sheet or fullScreenCover when you should use navigation, or vice versa. If the user is moving deeper into your app's structure (list to detail to edit), use navigation. If they're completing a separate task that doesn't fit the hierarchy (like changing settings or confirming a choice), use a sheet.
Frequently Asked Questions
What's the difference between NavigationStack and NavigationLink?
NavigationStack is newer (iOS 16+) and automatically manages the back button and the path through multiple screens. NavigationLink is older and works on iOS 15 and earlier, but requires more setup and is easier to use incorrectly. If your app supports iOS 16 or later, use NavigationStack.
How do I pass data from a list to a detail screen?
Define a property on the detail view that holds the data (like a product ID). When you create the NavigationLink, pass the data to that property. SwiftUI will show the detail screen with the correct data. If the user edits the data, use @State or @Binding to keep both screens in sync.
Can I navigate without the user tapping anything?
Yes. Use @State to track which screen should show, and use the navigationDestination() modifier to say "if this condition is true, show this screen." This is useful for automatically moving to a home screen after login, or showing an error screen when something fails.
When should I use a sheet instead of navigation?
Use a sheet when the user is completing a separate task that doesn't fit your app's main structure — like a settings form, a confirmation dialog, or a login screen. Use navigation when they're moving deeper into your app's hierarchy, like from a list to a detail to an edit screen.
What happens if I nest NavigationLinks inside each other?
The user can get confused about how to navigate back, or they might get stuck on a screen with no clear way to return. Keep your navigation flat — one NavigationStack at the top, with NavigationLinks inside, not nested inside each other.