Create an Interactive Widget Using App Intents
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:
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 ✌️