A SwiftUI WidgetKit Deep Link Tutorial

WidgetKit deep links allow the individual views that make up the widget entry view to open different screens within the companion app when tapped. In addition to the main home screen, the WidgetDemo app created in the preceding chapters contains a detail screen to provide the user with information about different weather systems. As currently implemented, however, tapping the widget always launches the home screen of the companion app, regardless of the current weather conditions.

The purpose of this chapter is to implement deep linking on the widget so that tapping the widget opens the appropriate weather detail screen within the app. This will involve some changes to both the app and widget extension.

Adding Deep Link Support to the Widget

Deep links allow specific areas of an app to be presented to the user based on the opening of a URL. The WidgetDemo app used in the previous chapters consists of a list of severe storm types. When a list item is selected, the app navigates to a details screen where additional information about the selected storm is displayed. In this tutorial, changes will be made to both the app and widget to add deep link support. This means, for example, that when the widget indicates that a thunder storm is in effect, tapping the widget will launch the app and navigate to the thunder storm detail screen.

The first step in adding deep link support is to modify the WeatherEntry structure to include a URL for each timeline entry. Edit the WeatherData.swift file and modify the structure so that it reads as follows:

.
.
struct WeatherEntry: TimelineEntry {
    var date: Date
    let city: String
    let temperature: Int
    let description: String
    let icon: String
    let image: String
    let url: URL?
}
.
.

Next, add some constants containing the URLs which will be used to identify the storm types that the app knows about:

.
.
let hailUrl = URL(string: "weatherwidget://hail")
let thunderUrl = URL(string: "weatherwidget://thunder")
let tropicalUrl = URL(string: "weatherwidget://tropical")
.
.

The last remaining change to the weather data is to include the URL within the sample timeline entries:

.
.
let londonTimeline = [
    WeatherEntry(date: Date(), city: "London", temperature: 87, 
          description: "Hail Storm", icon: "cloud.hail", 
                image: "hail", url: hailUrl),
    WeatherEntry(date: Date(), city: "London", temperature: 92, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder", url: thunderUrl),
    WeatherEntry(date: Date(), city: "London", temperature: 95,   
          description: "Hail Storm", icon: "cloud.hail", 
                image: "hail", url: hailUrl)
]
 
let miamiTimeline = [
    WeatherEntry(date: Date(), city: "Miami", temperature: 81, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder", url: thunderUrl),
    WeatherEntry(date: Date(), city: "Miami", temperature: 74,
          description: "Tropical Storm", icon: "tropicalstorm", 
                image: "tropical", url: tropicalUrl),
    WeatherEntry(date: Date(), city: "Miami", temperature: 72, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder", url: thunderUrl)
]
.
.

With the data modified to include deep link URLs, the widget declaration now needs to be modified to match the widget entry structure. First, the placeholder() and getSnapshot() methods of the provider will need to return an entry which includes the URL. Edit the WeatherWidget.swift file, locate these methods within the IntentTimelineProvider structure and modify them as follows:

struct Provider: IntentTimelineProvider {
   func placeholder(in context: Context) -> WeatherEntry {
       
        WeatherEntry(date: Date(), city: "London",
                           temperature: 89, description: "Thunder Storm",
                                icon: "cloud.bolt.rain", image: "thunder", 
                                    url: thunderUrl)
    }
 
    func getSnapshot(for configuration: ConfigurationIntent, with context: Context, completion: @escaping (WeatherEntry) -> ()) {
       
        let entry = WeatherEntry(date: Date(), city: "London", 
                      temperature: 89, description: "Thunder Storm", 
                        icon: "cloud.bolt.rain", image: "thunder", 
                         url: thunderUrl)
        completion(entry)
    }
.
.

Repeat this step for both declarations in the preview provider:

struct WeatherWidget_Previews: PreviewProvider {
    static var previews: some View {
       
        Group {
            WeatherWidgetEntryView(entry: WeatherEntry(date: Date(), 
                        city: "London", temperature: 89, 
                 description: "Thunder Storm", icon: "cloud.bolt.rain", 
                       image: "thunder", url: thunderUrl))
                .previewContext(WidgetPreviewContext(
                                      family: .systemSmall))
        
            WeatherWidgetEntryView(entry: WeatherEntry(date: Date(), 
                        city: "London", temperature: 89, 
                 description: "Thunder Storm", icon: "cloud.bolt.rain", 
                       image: "thunder", url: thunderUrl))
                 .previewContext(WidgetPreviewContext(
                                      family: .systemMedium))
        }
    }
}

The final task within the widget code is to assign a URL action to the widget entry view. This is achieved using the widgetUrl() modifier, passing through the URL from the widget entry. Remaining in the WeatherWidget.swift file, locate the WeatherWidgetEntryView declaration and add the modifier to the top level ZStack as follows:

struct WeatherWidgetEntryView : View {
    var entry: Provider.Entry
 
    @Environment(\.widgetFamily) var widgetFamily
    
    var body: some View {
 
        ZStack {
            Color("weatherBackgroundColor")
   
            HStack {
                WeatherSubView(entry: entry)
                if widgetFamily == .systemMedium {
                    ZStack {
                        Image(entry.image)
                            .resizable()
                    }
                }
            }
        }
        .widgetURL(entry.url)
    }
}

With deep link support added to the widget the next step is to add support to the app.

Adding Deep Link Support to the App

When an app is launched via a deep link, it is passed a URL object which may be accessed via the top level view in the main content view. This URL can then be used to present different content to the user than would normally be displayed.

The first step in adding deep link support to the WidgetDemo app is to modify the ContentView.swift file to add some state properties. These variables will be used to control which weather detail view instance is displayed when the app is opened by a URL:

import SwiftUI
 
struct ContentView: View {
    
    @State private var hailActive: Bool = false
    @State private var thunderActive: Bool = false
    @State private var tropicalActive: Bool = false
    
    var body: some View {
        NavigationView {
            List {

The above state variables now need to be referenced in the navigation links within the List view:

var body: some View {
    
    NavigationView {
        List {
            NavigationLink(destination: WeatherDetailView(
                   name: "Hail Storms", icon: "cloud.hail"), 
                       isActive: $hailActive) {
                Label("Hail Storm", systemImage: "cloud.hail")
            }
 
            NavigationLink(destination: WeatherDetailView(
                   name: "Thunder Storms", icon: "cloud.bolt.rain"), 
                      isActive: $thunderActive) {
                Label("Thunder Storm", systemImage: "cloud.bolt.rain")
            }
            
            NavigationLink(destination: WeatherDetailView(
                   name: "Tropical Storms", icon: "tropicalstorm"), 
                      isActive: $tropicalActive) {
                Label("Tropical Storm", systemImage: "tropicalstorm")
            }
        }
        .navigationTitle("Severe Weather")
    }
}

The isActive argument to the NavigationLink view allows the link to be controlled programmatically. For example, the first link will navigate to the WeatherDetailView screen configured for hail storms when manually selected by the user. With the addition of the isActive argument, the navigation will also occur if the hailActive state property is changed to true as the result of some other action within the code.

When a view is displayed as the result of a deep link, the URL used to launch the app can be identified using the onOpenUrl() modifier on the parent view. By applying this modifier to the NavigationView we can write code to modify the state properties based on the URL, thereby programmatically triggering navigation to an appropriately configured detail view.

Modify the ContentView declaration to add the onOpenUrl() modifier as follows:

struct ContentView: View {
    
    @State private var hailActive: Bool = false
    @State private var thunderActive: Bool = false
    @State private var tropicalActive: Bool = false
    
    var body: some View {
        
        NavigationView {
            List {
.
.                
                NavigationLink(destination: WeatherDetailView(name: "Tropical Storms", icon: "tropicalstorm"), isActive: $tropicalActive) {
                    Label("Tropical Storm", systemImage: "tropicalstorm")
                }
                
            }
            .navigationTitle("Severe Weather")
            .onOpenURL(perform: { (url) in
                self.hailActive = url == hailUrl
                self.thunderActive = url == thunderUrl
                self.tropicalActive = url == tropicalUrl
            })
        }
    }
}

The added code performs a comparison of the URL used to launch the app with each of the custom URLs supported by the widget. The result of each comparison (i.e. true or false) is then assigned to the corresponding state property. If the URL matches the thunder URL, for example, then the thunderActive state will be set to true causing the view to navigate to the detail view configured for thunder storms.

Testing the Widget

After making the changes, run the app on a device or simulator and make sure that tapping the widget opens the app and displays the detail screen correctly configured for the current weather.

Figure 50-1

Summary

By default, a widget will launch the main view of the companion app when tapped by the user. This behavior can be enhanced by establishing deep links that take the user to specific areas of the app. This involves using the widgetUrl() modifier to assign destination URLs to the views in a widget entry layout. Within the app the onOpenUrl() modifier is then used to identify the URL used to launch the app and initiate navigation to the corresponding view.