WidgetKit: Build Interactive Widgets for iOS 17
Buttons, toggles, and real functionality on the Home Screen
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:
- File → New → Target
- Widget Extension
- Name it (e.g., “MyAppWidget”)
- 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