NavigationView hacks for SwiftUI
There’s a couple ways to programmatically navigate with SwiftUI’s NavigationView, but they usually require you to still have a NavigationLink somewhere. NavigationLink is a bit inflexible when it comes to styling, so a workaround I’ve usually seen is to zero out the frame, maybe set opacity to 0, and maybe set .disabled(true):
struct ContentView: View {
@State private var isVisiting = false
var body: some View {
NavigationView {
VStack {
NavigationLink(
destination: Text("hello world"),
isActive: $isVisiting
) {
EmptyView()
}
.frame(width: 0, height: 0)
.opacity(0)
.disabled(true)
Text("Visit").onTapGesture {
self.isVisiting = true
}
}
}
}
}
Rather than having to duplicate these styles everywhere, I extracted a component called NavigationTarget:
struct NavigationTarget<Content: View>: View {
var destination: Content
@Binding var isActive: Bool
var body: some View {
NavigationLink(
destination: destination,
isActive: $isActive
) {
EmptyView()
}
.frame(width: 0, height: 0)
.opacity(0)
.disabled(true)
}
}
And it’s used like so:
struct ContentView: View {
@State private var isVisiting = false
var body: some View {
NavigationView {
VStack {
NavigationTarget(
destination: Text("hello world"),
isActive: $isVisiting
)
Text("Visit").onTapGesture {
self.isVisiting = true
}
}
}
}
}
That’s a bit nicer, but it’s still a bit of a bummer to have to litter views with these components that don’t actually show up in the UI. We also need to keep a @State variable for each NavigationTarget in our view.
It’d be nicer if we could just add a tapNavigatesTo` modifier so that tapping a view would navigate somewhere.
We can do that:
private struct TapNavigatesTo<DestinationType: View>: ViewModifier {
@State private var isActive = false
var destination: DestinationType
func body(content: Content) -> some View {
content
.highPriorityGesture(TapGesture().onEnded {
self.isActive = true
})
.overlay {
NavigationTarget(
destination: destination,
isActive: $isActive
)
}
}
}
extension View {
// Wrapper around NavigationTarget that navigates to destination here when you tap the view
func tapNavigatesTo<DestinationType: View>(destination: DestinationType) -> some View {
modifier(TapNavigatesTo(destination: destination))
}
}
Note that .overlay is only available as of iOS 15.
Now our view just looks like this:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Visit").tapNavigatesTo(destination: Text("hello world"))
}
}
}
Hi, I’m Pat. I'm writing about some stuff I figured out or thought was useful, but I am no expert on iOS development. Hopefully something here was useful for you but if I’m completely wrong, my bad.