Interactive Widgets in SwiftUI

Bruno Lorenzo
4 min readDec 12, 2023

Widgets are a great way to show users relevant information on their home screen.

Previous to iOS 17, we could only display static data. However, in WWDC23 Apple announced the possibility to add interactivity in widgets. This could be seen as an insignificant feature, but we can leverage it to allow our users to perform simple actions directly from the widget.

So, If you’re interested in improving your app’s engagement, here is how to use it.

We'll use a simple widget created for our Coffee Shop Demo App. The widget shows the last coffee the user ordered and a button to repeat the order.

The Widget

I'll not be going into details on how to create a Widget, but here's a simple guide.

If you already have your widget implemented, you can skip this section.

1. Add a Widget Extension Target

2. Define Widget's content

Implement TimeEntry protocol and define any data you want to display.

import WidgetKit

struct CoffeeOrder: TimelineEntry {
var date: Date
let coffees: Int
let lastCoffee: String
}

Implement TimelineProvider protocol. The provider produces a timeline of TimelineEntry and will tell the widget when to render new data.

struct CoffeeProvider: TimelineProvider {
typealias Entry = CoffeeOrder

func placeholder(in context: Context) -> CoffeeOrder {
// Return a placeholder
}

func getSnapshot(in context: Context, completion: @escaping (CoffeeOrder) -> Void)
// Snapshot is used to display information in the Widget Gallery
}

func getTimeline(in context: Context, completion: @escaping (Timeline<CoffeeOrder>) -> Void) {
// The timeline to be used in the widget
}
}

Craft Widget's UI (the fun part 😄).

struct LastOrderView: View {
let order: CoffeeOrder

init(_ order: CoffeeOrder) {
self.order = order
}

var body: some View { ... }
}

3. Add Widget's configuration

struct CoffeeShopWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "com.blorenzo.coffeeshop.last-order",
provider: CoffeeProvider()) { order in
LastOrderView(order)
}
.supportedFamilies([.systemSmall])
}
}

@main
struct CoffeeShopWidgetBundle: WidgetBundle {
var body: some Widget {
CoffeeShopWidgetLiveActivity()
CoffeeShopWidget()
}
}

Adding interactivity

Ok, we have our widget, and we are showing some information. How do we do to perform an action?

We need to use AppIntents. An AppIntent is basically a configuration that allows us to execute a specific action outside our app's scope.

struct RepeatOrderIntent: AppIntent {
static var title: LocalizedStringResource = "Repeat Last Coffee"

init(){}

func perform() async throws -> some IntentResult {
OrdersManager.shared.repeatLastOrder()
return .result()
}
}

Inside the perform() method, we can execute the logic we want, and return a result. In our case, we just tell our OrdersManager to repeat the last order.

  • For our demo, this is enough. However, we can configure a lot more in anAppIntent.
  • The AppIntent must be created in the app's main target.
  • If you're curious, check Apple's documentation regarding AppIntent for more information.

Now that we have our AppIntent ready, we need a way to use it from our widget.

The only components that support interactivity so far are Buttons and Toggls. Each one has a new initializer available to use with an AppIntent.

extension Button {
public init<I: AppIntent>(
intent: I,
@ViewBuilder label: () -> Label
)
}

extension Toggle {
public init<I: AppIntent>(
isOn: Bool,
intent: I,
@ViewBuilder label: () -> Label
)
}

So, to wrap up, we just need to update our widget's UI and add a new button with the AppIntent we created.

struct LastOrderView: View {
let order: CoffeeOrder

init(_ order: CoffeeOrder) {
self.order = order
}

var body: some View {
VStack(alignment: .leading) {
Text("10 coffees = 1 free")
.font(.system(size: 12, weight: .ultraLight))
HStack {
HStack {
Text("\(order.coffees)")
.font(.system(size: 32, weight: .heavy))
.contentTransition(.numericText(value: Double(order.coffees)))
Text("/10")
.font(.system(size: 32, weight: .heavy))
}
Spacer()
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 24))
}
Text("Your last coffee")
.font(.system(size: 9, weight: .bold))
Text(order.lastCoffee)
.font(.system(size: 16, weight: .thin))
.frame(maxWidth: .infinity, alignment: .leading)

Spacer()
Button(intent: RepeatOrderIntent()) {
Label("Repeat", systemImage: "plus")
.font(.caption)
}
.tint(Color.black)
.foregroundColor(.white)
}
.containerBackground(for: .widget) {
Color.widgetBackground.opacity(0.5)
}
}
}

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on X for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy