Deep linking is what turns a normal app into a connected system:
- tap a link → open a specific screen
- push a notification → navigate into a flow
- share a URL → recipient opens the same state
- run a shortcut → perform an action in-app
But SwiftUI's navigation system changed a lot between iOS 15 → iOS 18 → iOS 20.
Here’s the clean, modern way to handle deep links using:
-
NavigationStack - global app state
- dependency injection
- URL parsing
- scene-based handlers
This is production-grade deep linking — not toy examples.
🧠 1. Define Your Deep Link Model
Start by defining all linkable destinations:
enum DeepLink: Equatable {
case profile(userID: String)
case settings
case feed(category: String)
}
This becomes the “navigation language” of your app.
🌐 2. Parse URLs Into DeepLink Types
Create a simple parser:
struct DeepLinkParser {
static func parse(_ url: URL) -> DeepLink? {
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
switch components?.host {
case "profile":
if let id = components?.queryItems?.first(where: { $0.name == "id" })?.value {
return .profile(userID: id)
}
case "settings":
return .settings
case "feed":
if let category = components?.queryItems?.first(where: { $0.name == "category" })?.value {
return .feed(category: category)
}
default:
break
}
return nil
}
}
Supported example URLs:
myapp://profile?id=123
myapp://settings
myapp://feed?category=tech
🔗 3. AppState Stores the Active Deep Link
@Observable
class AppState {
var deepLink: DeepLink?
}
Inject this state at the root:
@main
struct MyApp: App {
@State var appState = AppState()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
}
.onOpenURL { url in
appState.deepLink = DeepLinkParser.parse(url)
}
}
}
🧭 4. Central Navigation Handler
Inside your root navigation view:
struct RootView: View {
@Environment(AppState.self) private var appState
@State private var path: [DeepLink] = []
var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: DeepLink.self) { link in
destination(for: link)
}
}
.onChange(of: appState.deepLink) { link in
guard let link else { return }
handle(link)
}
}
func handle(_ link: DeepLink) {
path = [link] // resets + navigates
}
@ViewBuilder
func destination(for link: DeepLink) -> some View {
switch link {
case .profile(let id):
ProfileView(userID: id)
case .settings:
SettingsView()
case .feed(let category):
FeedView(category: category)
}
}
}
This is the modern way:
- navigation lives in a single place
- deep link → navigation path
- feature views stay clean and unaware of URLs
📲 5. Use Deep Links From Notifications
.onReceive(NotificationCenter.default.publisher(for: .deepLinkTrigger)) { notif in
if let url = notif.object as? URL {
appState.deepLink = DeepLinkParser.parse(url)
}
}
Triggering:
NotificationCenter.default.post(
name: .deepLinkTrigger,
object: URL(string: "myapp://profile?id=999")!
)
Great for:
- remote notifications
- background pushes
- Spotlight shortcuts
- Siri shortcuts
📥 6. Deep Linking From Inside the App (Shared Links)
Create a sharable URL anywhere:
func share(userID: String) -> URL {
URL(string: "myapp://profile?id=\(userID)")!
}
Perfect for referral flows, invites, or user-to-user sharing.
🛡 7. Safe Deep Link Guarding
Prevent navigation when the user is not logged in:
func handle(_ link: DeepLink) {
if !session.isLoggedIn {
path = [.settings] // redirect
return
}
path = [link]
}
Production apps always guard deep links.
🧩 8. Deep Linking & Dependency Injection
Example: a feed requires a service:
FeedView(
viewModel: FeedViewModel(
category: category,
service: environment.feedService
)
)
SwiftUI’s environment makes DI seamless.
🧪 9. Preview Testing Deep Link Navigation
#Preview("Profile Deep Link") {
RootView()
.environment(AppState(deepLink: .profile(userID: "preview")))
}
You can now visually test navigation for every deep link.
🚀 Final Thoughts
Deep linking is the glue that connects:
- external URLs
- notifications
- Siri shortcuts
- cross-app flows
- share links
- web → app navigation
With the modern approach:
- URLs become typed data (DeepLink)
- navigation is centralized
- features stay decoupled
- app state drives routing
- everything becomes predictable and testable
This transforms your SwiftUI app into a real, connected system — not just isolated screens.
Top comments (0)