← Back to list

WidgetKit: Build Interactive Widgets for iOS 17

Buttons, toggles, and real functionality on the Home Screen

Chandra Welim · 2026-03-15 02:01 · 1 claps · 4.1 min read
#ios-development #widgetkit #swiftui #apple #mobile-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Photo by Peter Muniz on Unsplash

Photo by Peter Muniz on Unsplash

WidgetKit: Build Interactive Widgets for iOS 17

Buttons, toggles, and real functionality on the Home Screen

Widgets used to be display-only. Show information. Tap to open app. That’s it.

iOS 17 changed everything. Widgets can now have buttons. Toggles. Interactive elements that perform actions without opening the app.

Mark a todo complete from the Home Screen. Play/pause music. Toggle a light. Widgets are now mini apps.

What’s New in iOS 17

Interactive widgets:

  • Buttons that perform actions
  • Toggles that change state
  • Intents that execute in the background
  • No app launch required

Animation:

  • Widgets can animate content
  • Smooth transitions on updates

Larger sizes:

  • iPad gets extra large widgets
  • StandBy mode on iPhone

Setting Up a Widget

If you don’t have a widget target:

  1. File → New → Target
  2. Widget Extension
  3. Name it (e.g., “MyAppWidget”)
  4. Include Configuration Intent (for configurable widgets)

Basic structure:

import WidgetKit
import SwiftUI

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MyWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("My Widget")
        .description("Shows important information")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Timeline Provider

Widgets update via timeline entries:

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), items: [])
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        let entry = SimpleEntry(date: Date(), items: loadItems())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        let items = loadItems()
        let entry = SimpleEntry(date: Date(), items: items)

        // Update in 15 minutes
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))

        completion(timeline)
    }

    private func loadItems() -> [Item] {
        // Load from shared container (App Groups)
        // ...
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let items: [Item]
}

Adding Interactivity

Step 1: Create an App Intent

import AppIntents

struct ToggleItemIntent: AppIntent {
    static var title: LocalizedStringResource = "Toggle Item"

    @Parameter(title: "Item ID")
    var itemId: String

    init() {}

    init(itemId: String) {
        self.itemId = itemId
    }

    func perform() async throws -> some IntentResult {
        // Toggle the item in your data store
        await DataStore.shared.toggleItem(id: itemId)

        // Reload widget timeline
        WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

        return .result()
    }
}

Step 2: Use Button with Intent

struct ItemRow: View {
    let item: Item

    var body: some View {
        HStack {
            Button(intent: ToggleItemIntent(itemId: item.id)) {
                Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
            }
            .buttonStyle(.plain)

            Text(item.title)
                .strikethrough(item.isCompleted)

            Spacer()
        }
    }
}

struct MyWidgetView: View {
    let entry: SimpleEntry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Tasks")
                .font(.headline)

            ForEach(entry.items.prefix(3)) { item in
                ItemRow(item: item)
            }
        }
        .padding()
    }
}

Step 3: Use Toggle with Intent

struct ToggleFeatureIntent: AppIntent {
    static var title: LocalizedStringResource = "Toggle Feature"

    @Parameter(title: "Enabled")
    var isEnabled: Bool

    init() {}

    init(isEnabled: Bool) {
        self.isEnabled = isEnabled
    }

    func perform() async throws -> some IntentResult {
        await Settings.shared.setFeatureEnabled(isEnabled)
        return .result()
    }
}

struct FeatureToggle: View {
    @Binding var isEnabled: Bool

    var body: some View {
        Toggle(
            "Feature",
            isOn: $isEnabled
        )
        .toggleStyle(SwitchToggleStyle())
    }
}

// In widget view
Toggle(
    isOn: entry.isFeatureEnabled,
    intent: ToggleFeatureIntent(isEnabled: !entry.isFeatureEnabled)
) {
    Text("Dark Mode")
}

Sharing Data with Your App

Widgets run in a separate process. Share data via App Groups:

1. Enable App Groups:

Target → Signing & Capabilities → App Groups

2. Shared container:

class DataStore {
    static let shared = DataStore()

    private let containerURL: URL

    init() {
        containerURL = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourapp.shared")!
    }

    var items: [Item] {
        get {
            let url = containerURL.appendingPathComponent("items.json")
            guard let data = try? Data(contentsOf: url) else { return [] }
            return (try? JSONDecoder().decode([Item].self, from: data)) ?? []
        }
        set {
            let url = containerURL.appendingPathComponent("items.json")
            let data = try? JSONEncoder().encode(newValue)
            try? data?.write(to: url)
        }
    }

    func toggleItem(id: String) async {
        var items = self.items
        if let index = items.firstIndex(where: { $0.id == id }) {
            items[index].isCompleted.toggle()
            self.items = items
        }
    }
}

Triggering Updates

From your app:

import WidgetKit

// Reload specific widget
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

// Reload all widgets
WidgetCenter.shared.reloadAllTimelines()

From widget intent:

func perform() async throws -> some IntentResult {
    // Perform action

    // Refresh widget
    WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

    return .result()
}

Widget Animations

Content transitions animate automatically in iOS 17:

struct AnimatedWidget: View {
    let entry: Entry

    var body: some View {
        VStack {
            Text("\(entry.count)")
                .font(.largeTitle)
                .contentTransition(.numericText())  // Animated number change

            Text(entry.status)
                .contentTransition(.interpolate)  // Smooth text transition
        }
        .animation(.default, value: entry.count)
    }
}

Deep Linking

Handle widget taps:

struct MyWidgetView: View {
    let entry: Entry

    var body: some View {
        Link(destination: URL(string: "myapp://item/\(entry.item.id)")!) {
            ItemContent(item: entry.item)
        }
    }
}

// In your app
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    handleDeepLink(url)
                }
        }
    }

    func handleDeepLink(_ url: URL) {
        // Parse URL and navigate
        if url.host == "item",
           let itemId = url.pathComponents.last {
            navigateToItem(itemId)
        }
    }
}

Best Practices

1. Keep it simple:

Widgets have limited space. Show essential info only.

2. Fast loading:

Timeline providers run in the background. Keep data loading fast.

3. Meaningful interactions:

Don’t add buttons just because you can. Each interaction should be valuable.

4. Graceful updates:

After an action, update the widget state immediately. Don’t wait for sync.

5. Test all sizes:

Different widget sizes have different layouts. Test each one.

6. Preview configurations:

#Preview(as: .systemSmall) {
    MyWidget()
} timeline: {
    SimpleEntry(date: .now, items: [.sample])
    SimpleEntry(date: .now, items: [.sample, .sample2])
}

The Bottom Line

Interactive widgets make the Home Screen useful. Users can take action without opening your app.

Add buttons for quick actions. Use toggles for settings. Keep interactions fast and meaningful.

Widgets are no longer just displays. They’re mini apps.

Quick Reference

// App Intent for button
struct MyIntent: AppIntent {
    func perform() async throws -> some IntentResult {
        // Action
        WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
        return .result()
    }
}

// Button with intent
Button(intent: MyIntent()) {
    Text("Tap Me")
}

// Toggle with intent
Toggle(isOn: value, intent: ToggleIntent(newValue: !value)) {
    Text("Setting")
}

// Reload widgets
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

// Deep link
Link(destination: URL(string: "myapp://path")!) {
    Content()
}

Further reading:

*iOS 26 Programming for Beginners is a clear, project-based path from zero to building apps with Swift 6 and Xcode 26. [Mastering Swift 6](https://amzn.to/4rtvbEy)* is the reference I point people to on concurrency, performance, and modern Swift patterns.

References


메타데이터
post_id
75e00a8eede8
slug
widgetkit-build-interactive-widgets-for-ios-17-75e00a8eede8
url
https://medium.com/@chandra.welim/widgetkit-build-interactive-widgets-for-ios-17-75e00a8eede8
canonical_url
https://medium.com/@chandra.welim/widgetkit-build-interactive-widgets-for-ios-17-75e00a8eede8
author_url
https://medium.com/@chandra.welim
status
ok
fetched_at
2026-07-13 06:23:13