Почему эта статья появилась на свет

Привет! На связи мобильный Flutter разработчик. Если ты читаешь это, значит ты столкнулся с ситуацией когда необходимо подружить iOS виджеты с Flutter приложением.

Когда передо мной впервые встала эта задача, я черпал информацию из различных статей на английском, а теперь решил собрать все в одном (так еще и на русском)

Скажу сразу, разрабатывать будем без дополнительных библиотек + нужно будет иметь базовые знания в SwiftUI (нам как Flutter - разработчикам этот декларативный фреймворк не покажется сложным).

Реализация: пошаговый план действий

Теперь перейдем к делу. Весь процесс можно разбить на 2 основных этапа:

1. Создание Widget Target

Первым делом нам нужно создать новый target в iOS части нашего Flutter проекта. Widget Extension - это по сути отдельное мини-приложение, которое работает независимо от основного приложения.

Что делаем в Xcode:

  1. Открываем iOS проект (ios/Runner.xcworkspace)

  2. Добавляем новый target: File → New → Target → Widget Extension

  3. Даем имя виджету (например, MyAppWidget)

  4. В разделе Sign&Capabilities настраиваем App Groups для обмена данными между приложением и виджетом (Важно! Он должен быть идентичен настроенному в таргете основного приложения)

Теперь у нас сгенерировались основные файлы по умолчанию:

import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), emoji: "?")
    }

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

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

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        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)
    }

//    func relevances() async -> WidgetRelevances<Void> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
}

struct MyAppWidgetEntryView : View {
    var entry: Provider.Entry

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

            Text("Emoji:")
            Text(entry.emoji)
        }
    }
}

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

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

#Preview(as: .systemSmall) {
    MyAppWidget()
} timeline: {
    SimpleEntry(date: .now, emoji: "?")
    SimpleEntry(date: .now, emoji: "?")
}
  • MyAppWidget.swift

    Теперь рассмотрим подробнее структуру данного файла.

    Provider: TimelineProvider - это основная структура виджета описывающая его поведение.

    • функция placeholder - показывается пока виджет загружается впервые

    • функция getSnapshot - быстрая версия виджета для галереи

    • функция getTimeline - самый важный метод! Определяет когда и как часто обновлять.

    Стратегии обновления:
    .atEnd - обновить после последней записи в timeline
    .after(date) - обновить в конкретное время (примечательно что система говорит что обновит его в указанное время, но не гарантирует этого)
    .never - не обновлять автоматически

    Общение натива и Flutter

    В Flutter части я создал класс который позволяет записывать данные в платформу и извлекать их, а также вручную обновлять состояние iOS виджетов.

    class WidgetPreferencesChannel {
      //! METHOD CHANNEL
      static const _platformChannel =
          MethodChannel('com.app.example/widgetPreferences');
    
      Future<String?> setValue(String key, String value) async {
        if (!Platform.isIOS) {
          return null;
        }
        try {
          //! PLATFORM KEY
          final result = await _platformChannel.invokeMethod('saveWidgetData', {
            'key': key,
            'value': value,
          });
    
          return result as String?;
        } catch (err) {
          debugPrint('Error $err');
          return null;
        }
      }
    
      Future<String?> getValue(String key) async {
        if (!Platform.isIOS) {
          return null;
        }
        try {
          //! PLATFORM KEY
          final result = await _platformChannel.invokeMethod('getWidgetData', {
            'key': key,
          });
    
          return result as String?;
        } catch (err) {
          debugPrint('Error $err');
          return null;
        }
      }
    
      Future<bool?> updateWidget(String kind) async {
        if (!Platform.isIOS) {
          return null;
        }
        try {
          //! PLATFORM KEY
          final result = await _platformChannel.invokeMethod('updateWidget', {
            'kind': kind,
          });
    
          return result as bool?;
        } catch (err) {
          debugPrint('Error $err');
          return false;
        }
      }
    }
    

    Ну и соответственный класс в iOS части:

    
    public class StorageHelper {
        static let storage = UserDefaults.init(suiteName: "group.com.app.example")
    
        public static func setValue(key: String, value: Any) {
            storage?.set(value, forKey: key)
        }
    
        public static func getString(key: String) -> String? {
            return storage?.string(forKey: key)
        }
    }
    
    • Удобный хелпер для записи данных

    import WidgetKit
    import Foundation
    
    // Для перехватывания events из flutter
    class WidgetPreferencesHandler {
        static func register(with messenger: FlutterBinaryMessenger) {
    //        !!! PLATFORM KEY
            let storageChannel = FlutterMethodChannel(
                name: "com.app.example/widgetPreferences",
                binaryMessenger: messenger
            )
            
            storageChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
                // Аргументы запроса !!! PLATFORM KEY
                guard let args = call.arguments as? [String: Any] else {
                    result(FlutterError(
                        code: "UNAVAILABLE",
                        message: "Требуется передать аргументы",
                        details: nil
                    ))
                    return
                }
                switch call.method {
                    
                // Сохранение данных !!! PLATFORM KEY
                case "saveWidgetData":
                    
                    // Ключ значения
                    guard let key = args["key"] as? String else {
                        result(FlutterError(
                            code: "UNAVAILABLE",
                            message: "Требуется передать ключ (key)",
                            details: nil
                        ))
                        return
                    }
                    
                    // Сохраняем в UserDefaults через ваш StorageHelper
                    StorageHelper.setValue(key: key, value: args["value"] as Any)
                    
                    // Возвращаем текущее сохранённое значение (или nil)
                    let savedValue = StorageHelper.getString(key: key)
                    result(savedValue)
                    
    //          Получение данных !!! PLATFORM KEY
                case "getWidgetData":
                    
                    guard let key = args["key"] as? String else {
                        result(FlutterError(
                            code: "UNAVAILABLE",
                            message: "Требуется передать ключ (key)",
                            details: nil
                        ))
                        return
                    }
                    
                    // Получаем значение из UserDefaults
                    let value = StorageHelper.getString(key: key)
                    result(value)
                    
    //          Обновление виджета !!! PLATFORM KEY
                case "updateWidget":
                    
                    guard let kind = args["kind"] as? String else {
                        result(FlutterError(
                            code: "UNAVAILABLE",
                            message: "Требуется передать имя виджета (kind)",
                            details: nil
                        ))
                        return
                    }
                    // Обновляем виджет, чей kind = переданному
                    WidgetCenter.shared.reloadTimelines(ofKind: kind)
                    result(true)
                
                default:
                    return
                }
            }
        }
    }
    
    • класс для передачи и получения данных от Flutter приложения

    С помощью StorageHelper вы сможете получать данные непосредственно в getTimeline вашего виджета:

    func getTimeline(in context: Context, completion: @escaping (Timeline<SalavatEntry>) -> Void) {
            let emojiValue = StorageHelper.getString("emoji")
            let entry = SimpleEntry(date: Date(), emoji: emoji,)
            let timeline = Timeline(entries: [entry], policy: .never)
            completion(timeline)
        }
    

    Вот так это выглядит в нашем приложении

    Готово! Теперь вы знаете как синхронизировать данные между приложением и iOS виджетами. Можете подробнее изучить WidgetKit , а также возможности SwiftUI.

Комментарии (2)


  1. Adil_network
    26.08.2025 12:07

    А можно ли таким же способом обновлять виджет в фоне, например раз в 15 минут?


    1. imanov_sh Автор
      26.08.2025 12:07

      Да, можно установив atEnd на 15 минут больше от текущего времени. Но опять таки: система этого не гарантирует.