Почему эта статья появилась на свет
Привет! На связи мобильный 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.
Adil_network
А можно ли таким же способом обновлять виджет в фоне, например раз в 15 минут?
imanov_sh Автор
Да, можно установив atEnd на 15 минут больше от текущего времени. Но опять таки: система этого не гарантирует.