Render scrolling lists efficiently with List and ForEach, and move between screens with NavigationStack and NavigationLink — the backbone of a multi-screen iOS app, including passing data to a detail view.
List renders a scrolling, native-styled collection efficiently, only building the rows on screen — the SwiftUI equivalent of a table view. You feed it identifiable data (or use ForEach with an id) so SwiftUI can track rows across updates. Lists also give you swipe actions, selection, and pull-to-refresh with simple modifiers.
struct Message: Identifiable { // Identifiable -> stable row identity
let id = UUID()
let text: String
}
struct MessageList: View {
let messages: [Message]
var body: some View {
List(messages) { message in // efficient: builds only visible rows
Text(message.text)
}
.refreshable { /* pull-to-refresh: reload data */ }
}
}
// List recycles rows as you scroll. Make the data Identifiable (or give ForEach
// an id) so SwiftUI tracks items across updates.A NavigationStack manages a stack of screens with a title bar and a back button. A NavigationLink pushes a destination view when tapped. This is how a list screen drills into a detail screen; the stack handles the push/pop and back navigation for you, replacing UIKit’s navigation controller.
struct AppView: View {
var body: some View {
NavigationStack {
List {
NavigationLink("Profile") { ProfileView() } // pushes on tap
NavigationLink("Settings") { SettingsView() }
}
.navigationTitle("Home")
}
}
}
// NavigationStack owns the back stack + title bar; NavigationLink pushes a
// destination. The stack handles push/pop + the Back button automatically.The common flow is a list where tapping a row opens a detail view for that item. You pass the item into the destination view as a property, either directly in a NavigationLink or via value-based navigation with navigationDestination for larger apps. This master-detail pattern is the heart of most iOS apps.
struct ProductList: View {
let products: [Product]
var body: some View {
NavigationStack {
List(products) { product in
// value-based navigation: link carries the item
NavigationLink(product.name, value: product)
}
.navigationDestination(for: Product.self) { product in
ProductDetail(product: product) // receives the tapped item
}
}
}
}
// list -> tap -> ProductDetail(product:). The standard master/detail flow.Apps with top-level sections use a TabView, where each tab is a destination. For flows that navigate in response to logic (deep links, "go home after checkout"), you bind the NavigationStack to a path you can modify in code. Together, TabView and a bound navigation path structure larger apps and enable programmatic control.
struct RootView: View {
@State private var path = NavigationPath() // drive navigation from code
var body: some View {
TabView {
NavigationStack(path: $path) {
HomeView()
.toolbar {
Button("Deep link") { path.append(Product.sample) } // navigate programmatically
}
}
.tabItem { Label("Home", systemImage: "house") }
SearchView().tabItem { Label("Search", systemImage: "magnifyingglass") }
}
}
}
// TabView = top-level sections; a bound NavigationPath = code-driven navigation.