Skip to main content
Widgets bring your app’s content directly to the home screen, lock screen, and Dynamic Island. With Expo Apple Targets, you can build WidgetKit widgets using Swift and SwiftUI while keeping your source code outside the generated iOS directory.
Evan Bacon wrote an in-depth blog post about building widgets in production: Expo x Apple Widgets.The Pillar Valley game uses this plugin for production widgets.

Getting Started

1

Generate a widget target

Run the create-target CLI to scaffold a new widget:
npx create-target widget
This creates a targets/widget/ directory with:
  • expo-target.config.js — Target configuration
  • index.swift — Main widget bundle
  • widgets.swift — Widget implementation
  • AppIntent.swift — Configurable parameters
  • WidgetLiveActivity.swift — Live Activities support
  • WidgetControl.swift — Control Center widgets
2

Configure the target

Edit targets/widget/expo-target.config.js:
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module.exports = {
  type: "widget",
  icon: "../../icons/widget.png",
  colors: {
    // Widget background color (referenced in Info.plist)
    $widgetBackground: "#DB739C",
    
    // Global accent color for buttons and interactive elements
    $accent: "#F09458",
    
    // Custom colors for SwiftUI
    gradient1: {
      light: "#E4975D",
      dark: "#3E72A0",
    },
  },
  images: {
    valleys: "../../valleys.png",
  },
  entitlements: {
    "com.apple.security.application-groups": ["group.bacon.data"],
  },
};
3

Run prebuild

Generate the Xcode project with the widget linked:
npx expo prebuild -p ios --clean
4

Develop in Xcode

Open the project and find your widget under expo:targets/widget:
xed ios
All changes you make in this folder are saved to targets/widget/ outside the iOS directory.

Widget Implementation

Here’s the default widget structure generated by the template:
import WidgetKit
import SwiftUI

struct Provider: AppIntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
    }

    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: configuration)
    }
    
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}

struct widgetEntryView : View {
    var entry: Provider.Entry

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

            Text("Favorite Emoji:")
            Text(entry.configuration.favoriteEmoji)
        }
    }
}

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

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            widgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}

Colors

The plugin provides special color constants that map to Xcode build settings:
Color NameBuild SettingPurpose
$accentASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAMEGlobal accent color for buttons when editing the widget
$widgetBackgroundASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAMEWidget background color
You can also define custom colors for use in SwiftUI:
colors: {
  $widgetBackground: "#DB739C",
  $accent: "#F09458",
  
  // Custom colors with light/dark mode support
  gradient1: {
    light: "#E4975D",
    dark: "#3E72A0",
  },
  
  // Or use a single color
  primaryColor: "steelblue",
}
Access these colors in Swift:
Color("gradient1")
Color("primaryColor")

Live Activities

Live Activities appear on the lock screen and in the Dynamic Island for real-time updates:
targets/widget/WidgetLiveActivity.swift
import ActivityKit
import WidgetKit
import SwiftUI

struct WidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties
        var emoji: String
    }

    // Fixed non-changing properties
    var name: String
}

struct WidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: WidgetAttributes.self) { context in
            // Lock screen/banner UI
            VStack {
                Text("Hello \(context.state.emoji)")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)

        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI regions
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom \(context.state.emoji)")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T \(context.state.emoji)")
            } minimal: {
                Text(context.state.emoji)
            }
            .widgetURL(URL(string: "https://www.expo.dev"))
            .keylineTint(Color.red)
        }
    }
}

Reloading Widgets

Trigger widget updates from your React Native app:
import { ExtensionStorage } from "@bacons/apple-targets";

// Reload all widgets
ExtensionStorage.reloadWidget();

// Reload a specific widget by kind
ExtensionStorage.reloadWidget("widget");
See the Sharing Data guide for details on syncing data between your app and widget.

Troubleshooting

If you experience build issues, React Native’s uncompiled source can cause problems with the Swift compiler and SwiftUI previews.

Clear SwiftUI preview cache

xcrun simctl --set previews delete all

Prebuild without React Native

For faster iteration during widget development:
npx expo prebuild --template ./node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean
This is for development only, not production builds.

Widget doesn’t appear on home screen

On iOS 18+, long press your app icon and select widget display options to transform the app icon into the widget.

Real-World Example

The Pillar Valley game uses this plugin for production widgets. Check out the blog post for implementation details: Expo x Apple Widgets.

Build docs developers (and LLMs) love