Статья

Разработка нативных виджетов iOS в Expo

Как я выпустил нативные виджеты главного экрана iOS в managed Expo с помощью Config Plugins, App Groups и общего локального хранилища — без ручной настройки Xcode.

Разработка нативных виджетов iOS в Expo

При создании современного приложения-утилиты красивого интерфейса внутри самого приложения уже недостаточно. Настоящее поле битвы за удержание пользователей — главный экран.

Когда я создавал 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 кольца прогресса и отслеживание с талисманом, которые видно с первого взгляда.