Master-Detail with NavigationStack

NavigationStack with value-based navigation is the modern approach to master-detail interfaces in SwiftUI.
Follow along with the code: iOS-Practice on GitHub
Basic Structure
struct MasterDetailExerciseView: View {
@State private var users: [User] = []
@State private var selectedUser: User?
@State private var isLoading = false
var body: some View {
List(users, selection: $selectedUser) { user in
NavigationLink(value: user) {
UserRow(user: user)
}
}
.navigationTitle("Users")
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
.task {
await loadUsers()
}
}
}
NavigationLink with Value
NavigationLink(value: user) {
UserRow(user: user)
}
Instead of specifying a destination view directly, pass a value. The destination is defined elsewhere.
navigationDestination
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
When any NavigationLink with a User value is tapped, show UserDetailView.
User Row View
struct UserRow: View {
let user: User
var body: some View {
HStack(spacing: 12) {
Image(systemName: user.avatarURL)
.font(.title)
.foregroundColor(.accentColor)
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.company)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
Detail View with Nested Data
struct UserDetailView: View {
let user: User
@State private var posts: [Post] = []
@State private var isLoading = false
var body: some View {
List {
Section("User Info") {
HStack(spacing: 16) {
Image(systemName: user.avatarURL)
.font(.system(size: 50))
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
.font(.title2)
.fontWeight(.bold)
Text(user.email)
.font(.subheadline)
.foregroundColor(.secondary)
Text(user.company)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.cornerRadius(4)
}
}
}
Section("Posts (\(posts.count))") {
if isLoading {
ProgressView()
} else if posts.isEmpty {
ContentUnavailableView("No Posts", systemImage: "doc.text")
} else {
ForEach(posts) { post in
PostRow(post: post)
}
}
}
}
.navigationTitle(user.name)
.navigationBarTitleDisplayMode(.inline)
.task {
await loadPosts()
}
}
private func loadPosts() async {
isLoading = true
posts = try? await api.fetchPosts(forUserId: user.id) ?? []
isLoading = false
}
}
Why Value-Based Navigation?
- Type-safe - Compiler checks navigation types
- Centralized - All destinations in one place
- Deep linking - Easy to restore navigation state
- Programmatic - Navigate by setting values
Requirements
Your model must be Hashable:
struct User: Identifiable, Hashable {
let id: Int
let name: String
// ...
}
Interview Tip
NavigationStack replaced NavigationView in iOS 16. Discuss the benefits: type safety, programmatic navigation, and better deep link support.