Почему эта статья появилась на свет
Привет! На связи мобильный Flutter разработчик. Если ты читаешь это, значит ты столкнулся с ситуацией когда необходимо подружить iOS виджеты с Flutter приложением.
Когда передо мной впервые встала эта задача, я черпал информацию из различных статей на английском, а теперь решил собрать все в одном (так еще и на русском)
Скажу сразу, разрабатывать будем без дополнительных библиотек + нужно будет иметь базовые знания в SwiftUI (нам как Flutter — разработчикам этот декларативный фреймворк не покажется сложным).
Реализация: пошаговый план действий
Теперь перейдем к делу. Весь процесс можно разбить на 2 основных этапа:
1. Создание Widget Target
Первым делом нам нужно создать новый target в iOS части нашего Flutter проекта. Widget Extension — это по сути отдельное мини-приложение, которое работает независимо от основного приложения.
Что делаем в Xcode:
-
Открываем iOS проект (ios/Runner.xcworkspace)
-
Добавляем новый target: File → New → Target → Widget Extension
-
Даем имя виджету (например, MyAppWidget)
-
В разделе 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.
-
ссылка на оригинал статьи https://habr.com/ru/articles/940678/
Добавить комментарий