Help us improve
Share bugs, ideas, or general feedback.
From ios-from-web-guide
MANDATORY for any screen with navigation. Invoke before writing a NavigationStack, NavigationLink, or navigationDestination.
npx claudepluginhub j-morgan6/ios-from-web-guide --plugin ios-from-web-guideHow this skill is triggered — by the user, by Claude, or both
Slash command
/ios-from-web-guide:swiftui-navigation-foundationsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
1. **Use `NavigationStack` + `navigationDestination(for:)`** — iOS 16+ API. Never `NavigationView` (deprecated) or `NavigationLink(destination:)` (destination-first style is effectively deprecated too).
Creates p5.js generative art with seeded randomness, noise fields, and interactive parameter exploration. Use for algorithmic art, flow fields, or particle systems.
Share bugs, ideas, or general feedback.
NavigationStack + navigationDestination(for:) — iOS 16+ API. Never NavigationView (deprecated) or NavigationLink(destination:) (destination-first style is effectively deprecated too).NavigationLink(value:) with a typed value and a hoisted navigationDestination(for: T.self). The value must be Hashable.navigationDestination(for:) modifiers to the root view that owns the NavigationStack. Placing them inside lazy containers (TabView(.page), LazyVStack, LazyVGrid) silently fails to register them.NavigationLink, use .buttonStyle(.borderless) — NOT .plain. .borderless claims tap ownership (so inner Buttons fire); .plain only removes visual styling and inner taps still bubble to the outer link. This rule is enforced by hook H-W-3.NavigationPath bound via NavigationStack(path: $router.path). Deep links / push-from-anywhere flows append to the path.NavigationStack per tab. Don't nest NavigationStacks inside NavigationStacks — state behaves oddly.MainView / the root tab container.// Root view — owns the NavigationStack AND every navigationDestination
struct MainView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
FeedView()
.navigationDestination(for: Post.self) { post in
PostDetailView(post: post)
}
.navigationDestination(for: User.self) { user in
ProfileView(user: user)
}
}
}
}
// Feed — pushes via value, not destination
struct FeedView: View {
@State private var viewModel = FeedViewModel()
var body: some View {
List(viewModel.posts) { post in
NavigationLink(value: post) {
FeedCardView(post: post)
}
}
}
}
Note navigationDestination(for: Post.self) lives on the root, not inside FeedView. The NavigationLink(value: post) hands the Post up the tree; the root resolves which view to push.
.borderless vs .plain trapSuppose a feed card contains a like button:
NavigationLink(value: post) {
VStack {
Text(post.title)
Button("Like") { viewModel.toggleLike(post) } // want this to fire
}
}
Without any buttonStyle, the tap on "Like" bubbles up to the NavigationLink and pushes the detail view.
// ❌ DOES NOT WORK — .plain removes styling but inner tap still bubbles
Button("Like") { viewModel.toggleLike(post) }
.buttonStyle(.plain)
// ✅ WORKS — .borderless claims tap ownership
Button("Like") { viewModel.toggleLike(post) }
.buttonStyle(.borderless)
This bit the Trays project. It compiles, it looks right, and it's wrong. Hook H-W-3 flags .buttonStyle(.plain) inside a NavigationLink subtree.
@MainActor
@Observable
final class Router {
var path = NavigationPath()
func openPost(_ post: Post) { path.append(post) }
func openProfile(_ user: User) { path.append(user) }
func popToRoot() { path = NavigationPath() }
}
struct MainView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
FeedView()
.environment(router)
.navigationDestination(for: Post.self) { PostDetailView(post: $0) }
.navigationDestination(for: User.self) { ProfileView(user: $0) }
}
}
}
Now any descendant can call @Environment(Router.self) var router and router.openPost(post) to push.
TabView(.page) silently does nothing// ❌
TabView {
ForEach(feeds) { feed in
FeedView(feed: feed)
.navigationDestination(for: Post.self) { ... } // never registers
}
}
.tabViewStyle(.page)
Fix: Hoist the .navigationDestination to the root outside the TabView.
They do — if the inner one uses .buttonStyle(.borderless):
NavigationLink(value: post) {
VStack {
Text(post.title)
NavigationLink(value: post.author) { Text(post.author.name) }
.buttonStyle(.borderless)
}
}
NavigationLink(destination:)// ❌ Old style — loses state on re-render, can't be deep-linked
NavigationLink(destination: PostDetailView(post: post)) {
FeedCardView(post: post)
}
Fix: Use NavigationLink(value: post) with a hoisted navigationDestination(for: Post.self).
HashableSymptom: Compiler error "Type 'Post' does not conform to 'Hashable'".
Fix: Declare Post: Hashable. Auto-synthesis requires all stored properties to be Hashable. See swiftui-equatable-hashable-for-diffing.
No dedicated template — the canonical shape lives in MainView.swift generated by ios-feature-scaffold.
swiftui-equatable-hashable-for-diffing — the Hashable requirement for navigation values.ios-feature-scaffold — registers new navigation destinations at the root NavigationStack automatically.