Skip to content

Build Your First Widget in SwiftUI — A Guide with Source Code

Want to bring widgets to your SwiftUI app but not sure where to begin? This updated tutorial walks you through the entire process of creating a SwiftUI Widget using WidgetKit, complete with step-by-step guidance and working code. By the end, you’ll have a motivational widget that refreshes every 10 minutes with custom colors and messages.

🔗 Download the full commented source code (Xcode Project) for this Tutorial

🧰 Getting Started — Create a Fresh SwiftUI Project

Open Xcode and start a new app:

  • Go to File → New → Project → App

  • Name your project something like WidgetApp

  • Interface: SwiftUI

  • Language: Swift

Save the project and move on to the next step.

➕ Add the Widget Extension

Now it’s time to add a widget target:

  • File → New → Target

  • Select Widget Extension

  • Uncheck the options:

    • Live Activity

    • Control

    • Configuration App Intent

  • Name it something like WidgetAppWidget

  • Confirm and activate the new scheme if prompted.

🔍 Understand the Auto-Generated Files

Xcode will create two key files:

AppWidgetBundle.swift – Entry Point

This struct defines your widget bundle and serves as the extension’s entry:

import WidgetKit
import SwiftUI

@main
struct AppWidgetBundle: WidgetBundle {
    var body: some Widget {
        AppWidget()
    }
}
  • The @main attribute designates this as the starting point of your widget extension.

  • You can group multiple widgets into this bundle in future updates.

AppWidget.swift – The Core Widget File

This file contains everything your widget needs:

  • A data model (SimpleEntry)

  • A timeline provider

  • A view for the widget UI

  • The actual widget struct with configuration

  • A #Preview section to test in Xcode

📦 1. Define the Widget’s Data Model

The SimpleEntry struct represents the snapshot of data shown at each update:

struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
}
  • date: Tells the system when to show this entry.

  • emoji: Temporary example content — later, we’ll replace it with a message and background color.

🧠 2. Implement the TimelineProvider

The TimelineProvider tells WidgetKit what to display and when. It includes three main functions:

struct Provider: TimelineProvider {
   // 1️⃣ func placeholder(in context: Context) -> SimpleEntry { … }
   // 2️⃣ func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { … }
   // 3️⃣ func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) { … }
}

1️⃣ placeholder(in:)

Shows temporary content in the widget gallery.

func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), emoji: "😀")
}

2️⃣ getSnapshot(in:)

Provides a static snapshot of data.

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    let entry = SimpleEntry(date: Date(), emoji: "😀")
    completion(entry)
}

3️⃣ getTimeline(in:)

Generates a list of timeline entries.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []

    let currentDate = Date()
    for hourOffset in 0 ..< 5 {
        let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
        let entry = SimpleEntry(date: entryDate, emoji: "😀")
        entries.append(entry)
    }

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

🖼️ 3. Create the Widget UI with SwiftUI

Use SwiftUI to define how your widget appears on the home screen.

struct AppWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)
            Text("Emoji:")
            Text(entry.emoji)
        }
    }
}

🧱 4. Configure the Widget Structure

This section ties everything together and registers the widget with iOS.

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                AppWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                AppWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

👀 5. Previewing Your Widget

SwiftUI’s new #Preview lets you simulate the widget in Xcode.

#Preview(as: .systemSmall) {
    AppWidget()
} timeline: {
    SimpleEntry(date: .now, emoji: "😀")
    SimpleEntry(date: .now, emoji: "🤩")
}

🎯 Make It Useful — Show Random Motivational Quotes

Let’s make this widget more inspiring.

Instead of an emoji, we’ll show motivational quotes every 10 minutes and change background colors.

🛠 Update SimpleEntry

struct SimpleEntry: TimelineEntry {
    let date: Date
    let message: String
    let color: Color
}

🧠 Update Provider

Add sample messages and colors:

let messages = [
    "Keep going 💪", "You're doing great!", "Just one more step 🚶",
    "Stay focused 🎯", "You’ve got this 🔥", "Make it count 💥",
    "Smile, breathe, move 🌿", "Every moment matters ⏳"
]

let backgroundColors: [Color] = [
    .blue, .green, .orange, .pink, .purple, .yellow, .teal, .mint
]

let defaultColor: Color = .gray
let defaultMessage = "Keep going 💪"

Update placeholder and getSnapshot:

func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), message: defaultMessage, color: defaultColor)
}

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    let entry = SimpleEntry(date: Date(), message: defaultMessage, color: defaultColor)
    completion(entry)
}

Update getTimeline:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []
    let currentDate = Date()

    for minuteOffset in 0 ..< 6 {
        let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset * 10, to: currentDate)!
        let message = messages.randomElement() ?? defaultMessage
        let backgroundColor = backgroundColors.randomElement() ?? defaultColor
        let entry = SimpleEntry(date: entryDate, message: message, color: backgroundColor)
        entries.append(entry)
    }

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

🎨 Redesign the Widget View

struct AppWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        ZStack {
            VStack(spacing: 10) {
                Text(entry.message)
                    .font(.headline)
                    .multilineTextAlignment(.center)
                    .minimumScaleFactor(0.5)
                    .lineLimit(1)
                    .foregroundStyle(.primary)

                Text(entry.date, style: .time)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

⚙️ Update Widget Config

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            let backgroundColor = entry.color.opacity(0.25)

            if #available(iOS 17.0, *) {
                AppWidgetEntryView(entry: entry)
                    .containerBackground(backgroundColor, for: .widget)
            } else {
                AppWidgetEntryView(entry: entry)
                    .padding()
                    .background(backgroundColor)
            }
        }
        .configurationDisplayName("Motivational Widget")
        .description("This Widget Will Keep You Motivated")
    }
}

🧪 Preview It

#Preview("Small Widget", as: .systemSmall) {
    AppWidget()
} timeline: {
    SimpleEntry(date: .now, message: "Keep going 💪", color: .orange)
    SimpleEntry(date: .now, message: "Stay focused 🎯", color: .purple)
}

📱 Test on a Real Device

  1. Run your main app target on a real device.

  2. Long-press the Home Screen → Tap “+” → Search for your widget → Add.

  3. Optionally tap and edit the widget for configuration.

Note: App Groups aren’t required if your widget shows static or internal data only.

✅ Final Notes

  • Code is tested on Xcode 16.2 and iOS 18.2 (as of June 2025)

  • Always check compatibility with newer Xcode versions

  • If you run into issues, let us know via comments

DOWNLOAD SOURCE CODE FOR THIS TUTORIAL

 

 

 

Comments (0)

Leave a Reply

Back To Top
Search