You are currently viewing How to Handle Tap Gestures on Widgets?

How to Handle Tap Gestures on Widgets?

If you’ve interacted with widgets before, you may have noticed that they all have a default behavior when tapped, which is to launch the host app. However, it’s important to know that this behavior can actually be customized based on the widget configuration.

Moreover, it’s worth noting that not all SwiftUI components will work as expected in a widget. For instance, Buttons no longer work, and Links only work under specific conditions.

If you’re interested in learning more about these topics, keep reading!


How Do Tap Gestures Work on Widgets?

Even though widgets are essentially SwiftUI views under the hood, they have their own quirks when it comes to handling tap gestures. When a widget detects taps, it will launch its host app, and this behavior cannot be altered. However, we can customize what happens after the app launches, in a similar way to how we use deep links to customize app actions after launch.

These are the quirks that we need to be aware of:

  1. SiwftUI Button does not work on widgets
  2. SwiftUI Link is supported on the systemMedium and systemLarge widget families
  3. SwiftUI Link is not supported on the systemSmall widget families

All these are important to consider when designing and developing widgets to ensure proper functionality.


Handling Tap Gestures on Widgets

The ‘Tap Me’ Widget

I have created a small widget that may seem useless, but it serves as a helpful example of how to trigger various actions based on a widget’s configuration when it is tapped. Take a look:

The ‘Tap Me’ widget

As you can see, when the ‘Tap Me’ widget is tapped, it will launch the host app and navigate to the corresponding view based on the widget’s current background color. I believe that by using this example, you will have a clear understanding of how everything works by the end of this article.

Supporting Custom Tap Actions on Widgets

As mentioned earlier, tapping on a widget is just like tapping on a deep link. Therefore, we must first design the deep links that correspond to each navigation action:

Deep LinkAction
tap-me-widget://show-blueShow blue color view after app launch
tap-me-widget://show-greenShow green color view after app launch
tap-me-widget://show-redShow red color view after app launch
tap-me-widget://show-orangeShow orange color view after app launch

Now, let’s say we have set up a custom intent (TapMeConfigurationIntent) as shown below:

Custom intent for 'Tap Me' widget in Xcode
Custom intent for ‘Tap Me’ widget

then we can implement the widget’s getTimeline() method in such a way:

struct TapMeWidgetEntry: TimelineEntry {
    let date: Date
    let backgroundColor: Color
    let deepLinkCommand: String
}

struct TapMeWidgetTimelineProvider: IntentTimelineProvider {

    func getTimeline(for configuration: TapMeConfigurationIntent,
                     in context: Context,
                     completion: @escaping (Timeline<TapMeWidgetEntry>) -> ()) {
        
        let backgroundColor = configuration.backgroundColor
        
        // Convert `BgColor` to `Color`
        let color = color(for: backgroundColor)

        // Get deep link comand based on `backgroundColor`
        let command = command(for: backgroundColor)
        
        let entry = TapMeWidgetEntry(
            date: Date(),
            backgroundColor: color,
            deepLinkCommand: command
        )
        
        let timeline = Timeline(entries: [entry], policy: .atEnd)
        completion(timeline)
    }
    
    /// Provide deep link command
    private func command(for bgColor: BgColor) -> String {
        switch bgColor {
        case .blue:
            return "show-blue"
        case .green:
            return "show-green"
        case .red:
            return "show-red"
        case .orange:
            return "show-orange"
        case .unknown:
            fatalError("Unknow color")
        }
    }
  
    // Some other implementation here
    // ...
    // ...
}

In the above code, we are essentially determining the deep link command based on the provided widget configuration (backgroundColor). We will then use this command to create a timeline entry and feed it to the timeline.

With all these in place, we can then head over to the widget view and use the widgetURL(_:) modifier to fire up the host app using our desired deep link.

struct TapMeWidgetView: View {
    
    let entry: TapMeWidgetEntry
    
    var body: some View {
        VStack {
            Text("Tap Me!")
                .font(.title)
        }
        .widgetURL(URL(string: "tap-me-widget://\(entry.deepLinkCommand)"))
        .containerBackground(for: .widget) {
            entry.backgroundColor
        }
    }
}

Notice in the code above how we pass the deep link command from the timeline provider to the widget view using entry.deepLinkCommand.

With that, the ‘Tap Me’ widget is now ready to send out various deep links based on the widget’s current configuration.

Looking for more information on creating configurable widgets? Check out my previous articles:

Handling Widget Deep Links in SwiftUI Apps

Now, let’s switch our focus to the host app. What we need to do is pretty straightforward. We just need to extract the deep link command from the received deep link URL and perform the desired action accordingly.

Here’s the host app implemented using SwiftUI:

struct ContentView: View {
    
    @State private var showRedView = false
    @State private var showBlueView = false
    @State private var showGreenView = false
    @State private var showOrangeView = false
    
    var body: some View {
        VStack {
            Image(systemName: "applelogo")
                .imageScale(.large)
                .foregroundColor(.accentColor)
                .padding()
            Text("Swift Senpai Widget Demo")
        }
        .padding()
        .sheet(isPresented: $showRedView) {
            RedView()
        }
        .sheet(isPresented: $showOrangeView) {
            OrangeView()
        }
        .sheet(isPresented: $showGreenView) {
            GreenView()
        }
        .sheet(isPresented: $showBlueView) {
            BlueView()
        }
        .onOpenURL { url in
            // Handle this deep link URL
            handleWidgetDeepLink(url)
        }
    }
    
    /// Extract deep link command from URL
    private func handleWidgetDeepLink(_ url: URL) {
        
        guard
            let scheme = url.scheme,
            let host = url.host else {
            // Invalid URL format
            return
        }
        
        guard scheme == "tap-me-widget" else {
            // The deep link is not trigger by widget
            return
        }
        
        switch host {
        case "show-red":
            showRedView = true
        case "show-blue":
            showBlueView = true
        case "show-orange":
            showOrangeView = true
        case "show-green":
            showGreenView = true
        default:
            break
        }
    }
}

As you can see in the handleWidgetDeepLink() function, the deep link command is equivalent to the URL host. Therefore, we can easily extract it using url.host. Once the command is extracted, we can then set the view state accordingly to present the corresponding view.

Handling Widget Deep Links in UIKit Apps

For UIKit apps, the fundamental concept remains unchanged. However, we do need to handle the deep link separetely when the app is in the background or when it is terminated. In the case of the app being in the background, we can handle it by utilizing the following scene delegate method:

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {

    // Handle deep link when app in background
    guard let url = URLContexts.first?.url else {
        return
    }
    
    handleWidgetDeepLink(url)
}

In the case where the app is terminated, then we will have to manually trigger the aforementioned method within the scene(_:willConnectTo:options:) method.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    guard let _ = (scene as? UIWindowScene) else { return }
    
    // Handle deeplink when app is terminated
    self.scene(scene, openURLContexts: connectionOptions.urlContexts)
}

That’s all it takes to create the ‘Tap Me’ widget. If you’re interested in exploring the code further, feel free to download the sample code here.


Using Links on Widgets

Before wrapping up, let’s talk a bit about links on widgets. As mentioned earlier, links only work on the systemMedium and systemLarge widget families. However, using links on widgets allows us to give more ways for the user to interact with the widget.

For example, if we would like to have a way for users to force trigger a red view on the host app using the ‘Tap Me’ widget, we can add a link that does so.

Link("Show Red View", destination: URL(string: "tap-me-widget://show-red")!)

Using links can definitely add a new level of functionality and user engagement to our widgets.


If you enjoy reading this article, feel free to check out my other articles related to WidgetKit. You can also follow me on Twitter, and subscribe to my newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.

Thanks for reading. 👨🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.