- Регистрация
- 23 Август 2023
- Сообщения
- 2 822
- Лучшие ответы
- 0
- Реакции
- 0
- Баллы
- 51
Offline
Почему эта статья появилась на свет
Привет! На связи мобильный Flutter разработчик. Если ты читаешь это, значит ты столкнулся с ситуацией когда необходимо подружить iOS виджеты с Flutter приложением.
Когда передо мной впервые встала эта задача, я черпал информацию из различных статей на английском, а теперь решил собрать все в одном (так еще и на русском)
Скажу сразу, разрабатывать будем без дополнительных библиотек + нужно будет иметь базовые знания в SwiftUI (нам как Flutter - разработчикам этот декларативный фреймворк не покажется сложным).
Реализация: пошаговый план действий
Теперь перейдем к делу. Весь процесс можно разбить на 2 основных этапа:
1. Создание Widget Target
Первым делом нам нужно создать новый target в iOS части нашего Flutter проекта. Widget Extension - это по сути отдельное мини-приложение, которое работает независимо от основного приложения.
Что делаем в Xcode:
Теперь у нас сгенерировались основные файлы по умолчанию:
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: "🤩")
}
Привет! На связи мобильный 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.