[SC]()

iOS. Apple. Indies. Plus Things.

Create an Interactive Widget Using App Intents

// Written by Jordan Morgan // Jul 2nd, 2023 // Read it in about 2 minutes // RE: Snips

This post is brought to you by Emerge Tools, the best way to build on mobile.

let concatenatedThoughts = """

Welcome to Snips! Here, you'll find a short, fully code complete sample with two parts. The first is the entire code sample which you can copy and paste right into Xcode. The second is a step by step explanation. Enjoy!

"""

The Scenario

Create an interactive widget that increments a global count, shared with its target app, from a button tap powered via App Intents.

Note: This snip requires two different files due to the Widget extension.

Starting with the main app target:

import SwiftUI
import AppIntents
import WidgetKit

// 1
class Counter {
    private static let sharedDefaults: UserDefaults = UserDefaults(suiteName: "group.examples.sjc")!
    
    static func incrementCount() {
        var count = sharedDefaults.integer(forKey: "count")
        count += 1
        sharedDefaults.set(count, forKey: "count")
    }
    
    static func currentCount() -> Int {
        sharedDefaults.integer(forKey: "count")
    }
}

// 2
struct ExampleIntent: AppIntent {
    static var title: LocalizedStringResource = "Increment Count"
    static var description = IntentDescription("Increments a shared count with the main app.")
    
    func perform() async throws -> some IntentResult {
        Counter.incrementCount()
        return .result()
    }
}

struct ContentView: View {
    @Environment(\.scenePhase) private var phase
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
        }
        .padding()
        .onChange(of: phase) {
            count = Counter.currentCount()
        }
    }
}

And, in the Widget extension:

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), count: "\(Counter.currentCount())")
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        completion(SimpleEntry(date: Date(), count: "\(Counter.currentCount())"))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let timeline = Timeline(entries: [SimpleEntry(date: Date(), count: "\(Counter.currentCount())")], policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let count: String
}

struct WidgetsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Count:")
            Text(entry.count)
            // 3
            Button(intent: ExampleIntent()) {
                Text("Increment Count")
            }
        }
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

struct Widgets: Widget {
    let kind: String = "Widgets"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetsEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

With that, the App Intent increments the shared count:

An interactive widget on iOS 17.

The Breakdown

Step 1
Here, we set up a shared struct that both the app target and the widget extension will use. It simply stores a count in user defaults with App Groups enabled (this allows both the app, and its widget extension, to access it).

Step 2
This is the most important part: the App Intent, ExampleIntent, is what powers the button tap in the widget. Here, it simply access the shared count, increments it and returns.

If you need to know how to setup a basic App Intent, see this snip.

Step 3
Finally, in the widget itself, we use a Button initializer that accepts an App Intent. We pass in our ExampleIntent to run. When it executes, WidgetKit will ask for a fresh timeline and thus will show our updated count in the widget (and in the app itself).

Until next time ✌️

···

Spot an issue, anything to add?

Reach Out.