При создании современного приложения-утилиты красивого интерфейса внутри самого приложения уже недостаточно. Настоящее поле битвы за удержание пользователей — главный экран.
Когда я создавал WaterPaw, свой минималистичный трекер воды, нативные виджеты iOS были обязательной функцией. Пользователь должен иметь возможность взглянуть на главный экран и проверить кольца потребления воды. Однако если вы работаете в экосистеме React Native / Expo, вы знаете, чем традиционно страшна эта задача: виджеты iOS требуют писать нативный код на Swift, настраивать собственные App Extensions и управлять целями подписи кода в Xcode — ручная работа полностью ломает бесшовный managed-процесс Expo.
Вот чистая автоматизированная архитектура, с помощью которой я выпустил нативные виджеты iOS на Expo Config Plugins и общем локальном хранилище.
Архитектурная задача: песочница между приложением и виджетом
Поскольку WaterPaw работает по принципу local-first и ориентирован на приватность, данные целиком остаются на устройстве. Однако виджет iOS работает в отдельном фоновом процессе операционной системы и не может читать стандартный AsyncStorage приложения или файлы его каталога документов.
Чтобы преодолеть этот разрыв без облачного backend, нужно настроить контейнер песочницы Apple — App Group:
[ WaterPaw React Native App ]
→ (Writes to Shared Suite)
→ [ iOS App Group Container ]
→ [ iOS Native Home Widget ] ← (Reads via Swift)Шаг 1: запись данных из React Native
Каждый раз, когда пользователь записывает выпитую воду на уровне JavaScript, нужно передать эти данные в общий App Group. Для нативной привязки можно использовать react-native-shared-group-preferences:
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);
}
};Шаг 2: чтение общих данных в Swift
В нативном каталоге виджета на Swift теперь можно мгновенно читать данные из того же App Group через UserDefaults(suiteName:). Всё остаётся локальным, мгновенным и на 100% приватным:
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()
ProgressView(value: entry.progress)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
}
.padding()
}
}Шаг 3: автоматизация Xcode через Expo Config Plugins 🛠️
Именно здесь у разработчиков обычно возникают проблемы. Если запустить npx expo prebuild, Expo полностью сотрёт все ручные изменения и цели Xcode, настроенные для виджета.
Чтобы сохранить полностью managed-процесс, мы выносим код Swift в отдельный каталог targets/ и используем Config Plugin (например, expo-target-extension) для автоматической нативной сборки. Он сообщает Expo, что нужно автоматически создавать цель виджета, подключать этапы компиляции Swift и добавлять права доступа App Group каждый раз:
{
"expo": {
"name": "WaterPaw",
"slug": "waterpaw",
"plugins": [
[
"expo-target-extension",
{
"targetName": "WaterPawWidget",
"dir": "targets/widgets",
"entitlements": {
"com.apple.security.application-groups": ["group.com.yourgroupname"]
}
}
]
]
}
}Если добавить эту конфигурацию в app.json, очистить каталог ios/ и выполнить npx expo prebuild --clean, нативная среда Xcode будет полностью настроена сразу.
Вывод: не выходите из managed-режима, автоматизируйте
Перенос рабочих процессов нативных расширений целиком в Config Plugins сохраняет проект чистым и рассчитанным на будущее. Вы сохраняете быстрые циклы итераций Expo и при этом предлагаете пользователям глубоко интегрированную системную функцию iOS без задержек.
При создании утилит дополнительные усилия, потраченные на то, чтобы данные были видны с главного экрана, сильно окупаются долгосрочной вовлечённостью. Я использовал эту архитектуру, чтобы реализовать в WaterPaw кольца прогресса и отслеживание с талисманом, которые видно с первого взгляда.
