Skip to Content
Course content

231: Practice Exercise: Building a Type-Safe Deep Linking System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Deep links are one of those features that seem simple until you have twenty of them and a handful of developers trying to maintain them. If you're just passing strings around your app like "profile_page" or "settings/notifications", you're essentially playing a game of "I hope I didn't typo this" every time you add a new screen. I've spent too many hours debugging a broken link because someone wrote "userProfile" in one file and "user_profile" in another.

The goal here is to move the "stringiness" of a URL to a single entry point and immediately convert it into a type-safe Swift enum. That way, the rest of your app doesn't even know URLs exist; it just knows how to handle a DeepLink case.

Defining our destinations

Let's imagine we're building a book-selling app. We need to handle links to specific books, category pages, and a search page. Instead of using a dictionary or a bunch of constants, I'm going to use an enum with associated values. This lets us bundle the necessary ID or query string directly with the route.

enum DeepLink {
    case book(id: String)
    case category(name: String)
    case search(query: String)
}

Now, we need a way to turn a URL into one of these cases. I like to keep this logic in a dedicated parser. It keeps the AppDelegate or SceneDelegate clean.

Turning URLs into types

I'll create a DeepLinkParser. I want this to be a simple utility that takes a URL and returns an optional DeepLink. If the URL is malformed or doesn't match our schema, we just return nil and let the app handle it (usually by just ignoring it or showing a 404 page).

struct DeepLinkParser {
    static func parse(url: URL) -> DeepLink? {
        guard url.scheme == "bookstore" else { return nil }
        
        let pathComponents = url.pathComponents.filter { $0 != "/" }
        
        guard let route = pathComponents.first else { return nil }
        
        switch route {
        case "book":
            // I'll grab the ID from the second component
            let bookId = pathComponents[1] 
            return .book(id: bookId)
            
        case "category":
            let categoryName = pathComponents[1]
            return .category(name: categoryName)
            
        case "search":
            // Search usually uses query parameters: bookstore://search?q=swift
            let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
            let query = components?.queryItems?.first(where: { $0.name == "q" })?.value
            return query.map { .search(query: $0) }
            
        default:
            return nil
        }
    }
}

Fixing a potential crash

Wait. Look at that book and category logic I just wrote: pathComponents[1]. I just did the exact thing I told you to avoid. If someone sends a link like bookstore://book without an actual ID, the app is going to crash with an "Index out of range" error. I've actually done this in production before—don't be like me.

Let's fix that by safely checking the count of the components before we try to access them. I'll refine the switch statement to ensure we actually have the data we need.

switch route {
case "book":
    guard pathComponents.count > 1 else { return nil }
    return .book(id: pathComponents[1])
    
case "category":
    guard pathComponents.count > 1 else { return nil }
    return .category(name: pathComponents[1])
    
case "search":
    let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
    let query = components?.queryItems?.first(where: { $0.name == "q" })?.value
    return query.map { .search(query: $0) }
    
default:
    return nil
}

Wiring it to the UI

Now that we have a type-safe enum, the actual navigation becomes trivial. You can just switch over the DeepLink object. Because it's an enum, the compiler will force you to handle every case, meaning you'll never forget to implement a route.

func handleDeepLink(_ link: DeepLink) {
    switch link {
    case .book(let id):
        print("Navigating to book details for ID: \(id)")
        // coordinator.showBook(id: id)
    case .category(let name):
        print("Opening category: \(name)")
        // coordinator.showCategory(name: name)
    case .search(let query):
        print("Performing search for: \(query)")
        // coordinator.showSearch(query: query)
    }
}

This pattern is a lifesaver. You've isolated the "unsafe" part of your app (parsing raw strings from the outside world) to one single function. Once the data passes through that gate, it's fully typed and safe to use throughout your entire navigation stack.




📋 Practical Task

Exercise: Adding a User Profile Route

You need to extend the BookStore deep linking system to support user profiles. Users should be able to click a link like bookstore://profile/username123 to view another user's profile.

Your Task:

  • Update the DeepLink enum to include a profile case that takes a username: String.
  • Modify the DeepLinkParser.parse(url:) method to handle the "profile" route. Ensure you implement the safety check to avoid index-out-of-range crashes if the username is missing.
  • Update the handleDeepLink(_:) function to include a print statement confirming that the app is navigating to the profile of the specific user.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.