Skip to content
All essays
iOSMarch 27, 202513 min

Widget Development in Swift: iOS Widgets Guide

Create iOS widgets with SwiftUI. Interactive widgets, timelines, and best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

Widgets provide quick access to information from your app. Learn to create beautiful, interactive iOS widgets.

Widget Extension Setup

Create Widget Extension

  1. 1File > New > Target
  2. 2Choose "Widget Extension"
  3. 3Name your widget
  4. 4Choose "Include Configuration Intent" for interactive widgets

Basic Widget Structure

swift
import WidgetKit
import SwiftUI

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            SimpleWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Simple Widget")
        .description("A simple widget example")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

struct SimpleWidgetEntry: TimelineEntry {
    let date: Date
    let title: String
    let description: String
}

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleWidgetEntry {
        SimpleWidgetEntry(
            date: Date(),
            title: "Widget Title",
            description: "Widget description"
        )
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleWidgetEntry) -> Void) {
        let entry = SimpleWidgetEntry(
            date: Date(),
            title: "Snapshot",
            description: "Snapshot content"
        )
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let currentDate = Date()
        let entry = SimpleWidgetEntry(
            date: currentDate,
            title: "Current Time",
            description: "Updated at \(currentDate.formatted(date: .omitted, time: .standard))"
        )

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

        completion(timeline)
    }
}

struct SimpleWidgetEntryView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(entry.title)
                .font(.headline)
            Text(entry.description)
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue.opacity(0.1))
    }
}

Multiple Widget Sizes

Adaptive Layout

swift
struct AdaptiveWidgetEntryView: View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family

    var body: some View {
        switch family {
        case .systemSmall:
            SmallWidgetView(entry: entry)
        case .systemMedium:
            MediumWidgetView(entry: entry)
        case .systemLarge:
            LargeWidgetView(entry: entry)
        case .systemExtraLarge:
            ExtraLargeWidgetView(entry: entry)
        @unknown default:
            SmallWidgetView(entry: entry)
        }
    }
}

struct SmallWidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Image(systemName: "star.fill")
                .font(.largeTitle)
            Text(entry.title)
                .font(.headline)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(
            LinearGradient(
                colors: [.blue, .purple],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        )
    }
}

struct MediumWidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(entry.title)
                    .font(.headline)
                Text(entry.description)
                    .font(.caption)
            }

            Spacer()

            Image(systemName: "star.fill")
                .font(.title)
                .foregroundColor(.yellow)
        }
        .padding()
    }
}

struct LargeWidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text(entry.title)
                    .font(.title2)
                    .fontWeight(.bold)
                Spacer()
            }

            Divider()

            VStack(alignment: .leading, spacing: 8) {
                Label("Feature 1", systemImage: "checkmark.circle")
                Label("Feature 2", systemImage: "checkmark.circle")
                Label("Feature 3", systemImage: "checkmark.circle")
            }
            .font(.caption)

            Spacer()

            Text(entry.description)
                .font(.caption2)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

Interactive Widgets

Widget Configuration

swift
struct ConfigurationWidget: Widget {
    let kind: String = "ConfigurationWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            ConfigurationWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Configurable Widget")
        .description("A widget with configuration options")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

struct ConfigurationIntent: AppIntent {
    static var title: LocalizedStringResource = "Configuration"
    static var description = IntentDescription("Configure the widget")

    @Parameter(title: "Background Color")
    var backgroundColor: AppEnum?

    static var backgroundColorParameter: Parameter<AppEnum> {
        Parameter(
            title: "Background Color",
            options: [
                AppEnum(1, "Blue"),
                AppEnum(2, "Green"),
                AppEnum(3, "Orange")
            ]
        )
    }
}

Interactive Buttons

swift
struct InteractiveWidgetEntryView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack(spacing: 12) {
            Text("Quick Actions")
                .font(.headline)

            HStack(spacing: 12) {
                Link(destination: URL(string: "myapp://action1")!) {
                    Label("Action 1", systemImage: "heart.fill")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)

                Link(destination: URL(string: "myapp://action2")!) {
                    Label("Action 2", systemImage: "star.fill")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

Data Sharing

App Group Setup

  1. 1Enable App Groups in capabilities
  2. 2Use shared UserDefaults or FileManager
swift
class WidgetDataSharing {
    static let shared = WidgetDataSharing()
    private let suiteName = "group.com.yourcompany.widget"

    private let defaults: UserDefaults

    init() {
        defaults = UserDefaults(suiteName: suiteName)!
    }

    func saveData<T: Encodable>(_ data: T, forKey key: String) {
        if let encoded = try? JSONEncoder().encode(data) {
            defaults.set(encoded, forKey: key)
        }
    }

    func getData<T: Decodable>(forKey key: String, type: T.Type) -> T? {
        guard let data = defaults.data(forKey: key),
              let decoded = try? JSONDecoder().decode(type, from: data) else {
            return nil
        }
        return decoded
    }
}

// Usage in main app
struct WidgetData: Codable {
    let title: String
    let value: Int
    let lastUpdated: Date
}

// Save from main app
let data = WidgetData(title: "Dashboard", value: 42, lastUpdated: Date())
WidgetDataSharing.shared.saveData(data, forKey: "widgetData")

// Load in widget
let provider = Provider()
if let data: WidgetData = WidgetDataSharing.shared.getData(forKey: "widgetData", type: WidgetData.self) {
    print("Widget data: \(data)")
}

Timeline Management

Multiple Timeline Entries

swift
struct Provider: TimelineProvider {
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        var entries: [SimpleWidgetEntry] = []

        let currentDate = Date()
        let calendar = Calendar.current

        // Create entries for next 24 hours
        for hour in 0..<24 {
            if let date = calendar.date(byAdding: .hour, value: hour, to: currentDate) {
                let entry = SimpleWidgetEntry(
                    date: date,
                    title: "Hour \(hour)",
                    description: "Widget content"
                )
                entries.append(entry)
            }
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Best Practices

  1. 1Performance: Keep widgets lightweight
  2. 2Updates: Update timeline efficiently
  3. 3Images: Use system icons or optimized images
  4. 4Data: Share data via App Groups
  5. 5Interactivity: Use widget intents for interactivity
  6. 6Testing: Test all widget sizes
  7. 7Deep Links: Handle deep links properly
  8. 8Refresh: Refresh content appropriately

Widgets extend your app's presence and provide quick value to users!

Related essays

Next essay
Core Data Complete Guide: iOS Persistence