Search code examples
swiftuideep-linkingios-universal-linksswiftui-navigationviewuniversal-link

How to handle opening a URL to load a specific app screen?


I want to implement a functionality where when a user clicks a URL to reset their password, it directs them to the "PasswordReset" screen in my app.

Currently, when I click the URL (ex: https://app.myApp.com/auth/reset?token=1234), it opens up to the "FirstView" screen instead of the "PasswordReset" screen.

I noticed when I print the URL in the "myApp struct", it prints the correct URL above, but if I print the URL in the "RootView struct", it shows up as nil. What steps can I take to ensure that the URL loads to the "PasswordReset" screen?

@main
struct myApp: App {
    @State var route: URL?

    var body: some Scene {
        WindowGroup {
            RootView(url: route)
                .onOpenURL { url in
                    let _ = print(url)  // prints: https://app.myApp.com/auth/reset?token=1234
                    self.route = url
                }
        }
    }
}

struct RootView: View {
    @ObservedObject var authenticator = Authenticator.shared
    @State var url: URL?

    var body: some View {
        let _ = print(url) // prints: nil 

        // if user clicks on url to reset password, PasswordResetView should load
        if url?.pathComponents.contains("reset") ?? false,
                  let resetToken = url?.queryDictionary?["token"] {
            NavigationView {
                PasswordResetView(viewModel: .init(onFinish: { url = nil }))
                    .onAppear {
                        self.authenticator.accessToken = resetToken
                    }
            }

        // if user had accessToken (can log in), the MainTabView should load
        } else if authenticator.accessToken != nil {
            MainTabView()

        // if user has no accessToken, FirstView should load
        } else {
            NavigationView {
               FirstView()
            }
        }
    }
}

extension URL {
    var queryDictionary: [String: String]? {
        guard let query = self.query else { return nil }

        var queryStrings = [String: String]()
        for pair in query.components(separatedBy: "&") {
            let key = pair.components(separatedBy: "=")[0]

            let value = pair
                .components(separatedBy: "=")[1]
                .replacingOccurrences(of: "+", with: " ")
                .removingPercentEncoding ?? ""

            queryStrings[key] = value
        }
        return queryStrings
    }
}

Solution

  • Here's my solution to this issue:

    struct myApp: App {
        @ObservedObject var authenticator = Authenticator.shared
        @State var route: URL?
        @State var urlToken: String?
        
        var body: some Scene {
            WindowGroup {
                RootView(url: route)
                    .onOpenURL { url in
                        self.route = url
                        if url.pathComponents.contains("reset"), let token = url.queryDictionary?["token"] {
                            self.urlToken = token
                        }
                    }
                    .fullScreenCover(item: $urlToken) { _ in
                        NavigationView {
                            PasswordResetView(viewModel: .init(onFinish: {
                                route = nil
                                urlToken = nil
                            }))
                            .onAppear {
                                self.authenticator.accessToken = urlToken
                            }
                        }
                    }
            }
        }
    }
    
    struct RootView: View {
        @ObservedObject var authenticator = Authenticator.shared
    
        var body: some View {
            if authenticator.accessToken != nil {
                MainTabView()
            } else {
                NavigationView {
                   FirstView()
                }
            }
        }
    }