Logo for tanaschita.com

Structuring universal links in iOS

A practical way to parse and route universal links using enums and type-safe navigation.

29 Sep 2025 · 6 min read

Universal links are a powerful way to create experiences between a website and an iOS app. As an app grows and supports more types of universal links, managing them can become complex without proper structure.

In this article, we'll look at a simple architecture that keeps universal link handling clean, testable, and easy to extend. Let's dive in.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

Handling a universal link usually means parsing the incoming URL, extracting parameters and routing to the right part of the app. To represent a universal link, we can use an enum which acts as the source of truth for every universal link our app understands:

enum UniversalLink {
/// Example: https://www.example.com/mobile-app/recipes
/// Switches to the recipes tab.
case recipes
/// Example: https://www.example.com/mobile-app/recipe?id=64ba8b48a92d44eb4e0ef89e
/// Opens a specific recipe.
case recipe(id: String)
/// Example: https://www.example.com/mobile-app/profile?to=settings
/// Opens a detail view inside the profile tab.
case profile(ProfileUniversalLinkDestination)
}
enum ProfileUniversalLinkDestination: String {
case settings
case favorites
}

This keeps the list of supported links visible in one place, enforces type safety, and makes it clear which parameters belong to which link.

For links with more complex payloads we can go beyond simple strings or enums. For example:

struct RecipeUniversalLinkPayload: Equatable {
let id: String
let referrer: String?
}
enum RecipeUniversalLinkPayload {
case recipe(RecipeUniversalLinkPayload)
}

If we need to support many related links (for example, several different links inside a recipes domain), we can also group them:

enum UniversalLink {
case recipes(RecipesUniversalLink)
}
enum RecipesUniversalLink {
case list
case detail(id: String)
}

Centralize parsing

To centralize all URL parsing logic in one place, we can use a failable or throwing initializer:

extension UniversalLink {
init?(from url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil }
guard let path = components.path.split(separator: "/").map(String.init).last else { return nil }
switch path {
case "recipes":
self = .recipes
case "recipe":
if let id = components.queryItems?.first(where: { $0.name == "id" })?.value {
self = .recipe(id: id)
} else {
return nil
}
case "profile":
...
default:
return nil
}
}
}

Instead of just returning nil, we could alternatively throw a concrete error to be able to show or log a specific error description.

Integrating with routing

Once we have a parsed UniversalLink, we need to integrate it with our app's navigation system. If you're using a router-based architecture (as described in this article on using NavigationPath with TabView), the integration becomes straightforward:

.onOpenURL { url in
if let link = UniversalLink(from: url) {
switch link {
case .recipes:
appRouter.navigateTo(tab: .recipes)
case .recipe(let id):
appRouter.navigateTo(tab: .recipes)
appRouter.recipesRouter.navigateTo(route: .detail(id: id))
case .profile(let destination):
appRouter.navigateTo(tab: .profile)
appRouter.profileRouter.navigateTo(destination)
}
}
}

Testing universal links

With this approach, we can ensure quality with unit testing which becomes straightforward. We can test each link type independently:

func testRecipeUniversalLink() {
let url = URL(string: "https://www.example.com/mobile-app/recipe?id=123")!
#expect(UniversalLink(from: url) == .recipe(id: "123"))
}

We can write similar tests for each link type to guarantee that URLs are interpreted correctly.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

navigation

design patterns

architecture

swiftui

How to use NavigationPath for routing in SwiftUI

Learn how to manage navigation in SwiftUI programmatically.

09 Jun 2024 · 5 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum