Menu

Notifications, Deep Links

singleTop

When a user taps a notification or deep link for your app on Android, the system fires an Intent to open your app. By default, this will initialize a new instance of your app’s main Activity, even if your app is already running. That means that any transient UI state may be lost.

If you’d like the more iOS-like behavior of keeping your UI as-is when your app is brought to the foreground via a notification or deep link, you can use the singleTop launch mode. Edit your Android/app/src/main/AndroidManifest.xml as follows:

<manifest ...>
    ...
    <application ...>
        ...
        <activity 
             ...
             android:launchMode="singleTop">
            ...
        </activity>
    </application>
</manifest>

Read more about launch modes and other Activity options here.


Notifications

Skip supports the core API of Apple’s UserNotifications framework so that your iOS notification-handling code works across platforms. The setup for integrating notification functionality into your app, however, will vary depending on the push service you are using. Skip’s Firebase support includes push messaging out of the box. Follow its instructions to support push notifications on top of Firebase Cloud Messaging in your dual-platform app.

You often want to take a user to a particular part of your app when they tap on a notification. To do so, we recommend sending a deep link URL in the notification metadata. The next section discusses how to support deep links in your cross-platform app, and our FireSide sample app demonstrates push notifications, deep linking, and using them together. Here is an excerpt showing how you might send a user to a location in your app from your UNUserNotificationCenterDelegate:

@MainActor
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
    // Look for a custom 'deep_link' key in the notification payload, which should be a URL with our app's scheme
    let content = response.notification.request.content
    if let deepLink = content.userInfo["deep_link"] as? String, let url = URL(string: deepLink) {
        await UIApplication.shared.open(url)
    }
}

Deep links allow you to bring a user to a particular part of your app. Skip supports custom URL schemes and SwiftUI deep link handling.

Darwin Setup

To support deep links in your iOS build, first follow Apple’s instructions to register your custom URL scheme in Xcode.

Xcode screenshot of adding a custom URL scheme

Only pay attention to the instructions for registering your custom URL scheme. Ignore the remaining instructions about handling deep links in your code, because you’ll be using SwiftUI’s deep link processing instead.

Android Setup

Edit your Android build’s AndroidManifest.xml to add an intent-filter for your custom URL scheme. For example to support myurlscheme, add the following to your AndroidManifest.xml:

<manifest ...>
    ...
    <application ...>
        ...
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.BROWSABLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="myurlscheme" />
            </intent-filter>
        </activity>
    </application>
</manifest>

While iOS is flexible, Android will expect deep link URLs with the general form scheme://host or scheme://host/path.

SwiftUI

SwiftUI uses the onOpenURL view modifier to intercept and process deep links. Place the modifier on a view that will be rendered when the app opens, and use its action to process the given URL. This will typically involve updating your navigation bindings to take the user to a specified location in the app, as in the following sample:

enum Tab : String {
    case cities, favorites, settings
}

public struct ContentView: View {
    @AppStorage("tab") var tab = Tab.cities
    @State var cityListPath = NavigationPath()

    public var body: some View {
        TabView(selection: $tab) {
            NavigationStack(path: $cityListPath) {
                CityListView()
            }
            ...
            .tag(Tab.cities)

            NavigationStack {
                FavoriteCityListView()
            }
            ...
            .tag(Tab.favorites)

            SettingsView()
                ...
                .tag(Tab.settings)
        }
        // travel://<tab>[/<city>], e.g. travel://cities/London or travel://favorites
        .onOpenURL { url in
            if let tabName = url.host(), let tab = Tab(rawValue: tabName) {
                self.tab = tab // Select the encoded tab
                if tab == .cities, let city = city(forName: url.lastPathComponent)) {
                    // iOS needs an async dispatch after switching tabs to read navigationDestinations
                    DispatchQueue.main.async {
                        // Set nav stack to root + specified city
                        cityListPath.removeLast(cityListPath.count)
                        cityListPath.append(city.id)
                    }
                }
            }
        }
    }
}

Testing

On iOS, the easiest way to test your deep link handling is by entering a URL with your custom scheme into Safari. You can also write a URL into a Calendar event or a Note. iOS will linkify the text so that tapping it will open your app.

Android includes an adb command for sending intents to the running emulator or device, including deep links. Building on our SwiftUI example above, enter a command like the following in Terminal:

% adb shell am start -W -a android.intent.action.VIEW -d "travel://cities/London"