How to build native iOS widgets in managed Expo using Config Plugins
When building a modern utility app, a gorgeous interface inside the application isn’t enough anymore. The real battleground for user retention is the home screen.

When I was building WaterPaw, my minimalist hydration tracker, native iOS Widgets were a non-negotiable feature. Users should be able to glance at their home screen, check their hydration rings.
However, if you work in the React Native / Expo ecosystem, you know the historical dread of this task: iOS widgets require writing native Swift, configuring custom App Extensions, and managing code-signing targets in Xcode. Doing this manually completely breaks the seamless, managed Expo workflow.
Here is the clean, automated architecture I used to ship native iOS widgets using Expo Config Plugins and shared local-first storage.
The Architectural Challenge: The App-to-Widget Sandbox
Because WaterPaw is local-first and privacy-focused, data stays entirely on the device. However, an iOS Widget runs in its own separate operating system background process. It cannot read your app’s standard AsyncStorage or document directory files.
To bridge this gap without spinning up a cloud backend, you must configure an Apple sandbox container called an App Group.
[ WaterPaw React Native App ] - (Writes to Shared Suite) -> [ iOS App Group Container ] -> [ iOS Native Home Widget ] <- (Reads via Swift)Step 1: Writing Data from React Native
Whenever a user logs water inside the JavaScript layer, we need to push that data into the shared App Group. We can use react-native-shared-group-preferences to handle the native bridge binding:
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const APP_GROUP = 'group.com.yourgroupname';
export const syncDataToWidget = async (currentMl: number, goalMl: number) => {
const dataForWidget = {
progress: currentMl / goalMl,
displayString: `${currentMl} / ${goalMl} ml`,
updatedAt: new Date().toISOString()
};
try {
await SharedGroupPreferences.setItem('WidgetData', dataForWidget, APP_GROUP);
} catch (error) {
console.error('Failed to sync data to iOS App Group Suite', error);
}
};
Step 2: Consuming the Shared Data in Swift
Inside your native Swift widget directory, you can now read from that exact same App Group suite instantaneously using UserDefaults(suiteName:). This keeps everything local, instant, and 100% private:
import WidgetKit
import SwiftUI
struct HydrationEntry: TimelineEntry {
let date: Date
let progress: Double
let displayString: String
}
struct WaterPawWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("WaterPaw")
.font(.caption)
.foregroundColor(.secondary)
Text(entry.displayString)
.font(.title2)
.bold()
// Minimalist native progress tracking line
ProgressView(value: entry.progress)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
}
.padding()
}
}Step 3: Automating Xcode via Expo Config Plugins 🛠️
This is where developers typically run into trouble. If you run npx expo prebuild, Expo will completely wipe out any manual Xcode modifications or targets you set up for the widget.
To maintain a fully managed workflow, we isolate our Swift code into a standalone targets/ directory and use a Config Plugin (like expo-target-extension) to automate the native assembly. This script instructs Expo to automatically create the Widget Target, link the Swift compilation phases, and inject the App Group entitlements into the generated build configuration every single time:
{
"expo": {
"name": "WaterPaw",
"slug": "waterpaw",
"plugins": [
[
"expo-target-extension",
{
"targetName": "WaterPawWidget",
"dir": "targets/widgets",
"entitlements": {
"com.apple.security.application-groups": ["group.com.yourgroupname"]
}
}
]
]
}
}By putting this setup into app.json, clearing your ios/ directory and running npx expo prebuild --clean completely configures the native Xcode environment out-of-the-box.
Conclusion: Don’t Eject, Automate
Moving native extension workflows entirely into Config Plugins keeps your mobile project clean and future-proof. You get to keep the fast iteration cycles of Expo while serving your users a deeply integrated, zero-latency iOS system feature.
When building utilities, the extra effort spent making your data glanceable on the home screen pays off drastically in long-term engagement.
I utilized this architecture to implement the glanceable progress rings and mascot tracking features inside WaterPaw app I built with React Native and Expo.

Replies