Мы все любим разрабатывать мобильные приложения с помощью React Native, который обеспечивает кроссплатформенную интеграцию и широко используется как для iOS, так и для Android. Но знаете, что еще интереснее?
Нетрудно догадаться, что речь идет о приложении на React Native, поддерживающем умные часы.
В этой статье вы узнаете, как интегрировать приложение Apple watchOS с приложением на React Native. Кроме того, мы создадим виджет, который можно установить в циферблате часов в качестве дополнительной функции.
Необходимые условия
- Базовые знания о React Native, Xcode и SwiftUI.
- Xcode с симулятором устройств с поддержкой watchOS.
Настройка среды разработки
Чтобы создать watchOS-приложение для приложения на базе React Native, будем использовать Xcode — интегрированную среду разработки, предоставляемую Apple.
Кроме того, понадобится WatchKit — фреймворк, предоставляемый Apple для создания watchOS-приложений. С помощью WatchKit можно не только создавать watchOS-приложения, но и программировать их для подключения и работы с приложениями на других устройствах Apple.
Установка необходимых зависимостей для watchOS и виджетов
Для реализации связи между iOS- и watchOS-приложениями будем использовать библиотеку react-native-watch-connectivity.
Чтобы установить этот пакет, можете использовать yarn или npm в зависимости от вашего проекта:
npm install react-native-watch-connectivity - save
yarn add react-native-watch-connectivity
Не забудьте установить менеджер зависимостей CocoaPods!
cd ios && pod install && cd ..
Теперь создадим самый базовый экран React Native, на котором будет переменная count с кнопками + и -.
import React, {useState} from 'react'; import {Button, Text, View} from 'react-native'; function App() { const [count, setCount] = useState(0); return ( <View style={{ height: '100%', flexDirection: 'column', justifyContent: 'center', }}> <View style={{alignItems: 'center'}}> <Button title="+" onPress={() => setCount(count => count + 1)} /> <Text style={{margin: 24}}>{count}</Text> <Button title="_" onPress={() => setCount(count => count - 1)} /> </View> </View> ); }
Добавление поддержки watchOS-приложения
Теперь, настроив приложение для iOS, начнем разработку watchOS-приложения.
Откройте проект iOS с помощью Xcode. На панели инструментов выберите File -> New -> Target.
Увидев следующее окно, выберите верхнюю вкладку watchOS, а под этой вкладкой выберите App.
Вы увидите еще одно окно, в котором нужно предоставить более подробную информацию о watchOS-приложении.
Укажите имя приложения и идентификатор пакета для приложения watchOS.
В окне, показанном выше, необходимо указать, есть ли у вас уже существующее iOS-приложение (existing iOS App). В данном случае у нас есть проект iOS-приложения. Поэтому выберем “Watch App for Existing iOS App”, а в выпадающем списке ниже — таргет iOS-приложения.
После нажатия кнопки Finish («Завершение») вы заметите, что в проект Xcode добавится новая папка. В этой папке находятся файлы проекта watchOS.
Разработка UI watchOS-приложения
Теперь приступим к разработке UI (пользовательского интерфейса) для watchOS-приложения. Для начала откройте файл ContentView.swift, который содержит код SwiftUI.
В коде SwiftUI отобразим простой текстовый элемент, который будет показывать значение счетчика.
ContentView должен выглядеть следующим образом:
struct ContentView: View { var Body: some View { VStack { Text("") } } }
Реализация функциональности
Теперь настроим механизм двусторонней связи между приложениями iOS и watchOS.
Отправка сообщений из iOS-приложения в watchOS-приложение
- iOS-приложение в качестве отправителя
Для отправки сообщений в watchOS-приложение потребуется использовать пакет, установленный нами в приложении на базе React Native.
Импортируем функции sendMessage и getReachability из react-native-watch-connectivity. Будем использовать их, чтобы определить доступность часов для отправки переменной count в watchOS-приложение.
Модифицируем действия с кнопками, как показано ниже:
import { sendMessage, getReachability } from 'react-native-watch-connectivity';
...
const [count, setCount] = useState(0);
<Button
title="+"
onPress={async () => {
const isReachable = await getReachability();
if (isReachable) {
const newCount = count + 1;
setCount(newCount);
// Этот метод отправит сообщение в приложение watchOS
sendMessage(
{ count: newCount },
(replyObj: any) => {
console.log("reply from watchOS app: ", replyObj);
},
(error) => {
console.log("error sending message: ", error);
}
);
}
}}
/>;
- watchOS-приложение в качестве приемника
Чтобы настроить watchOS-приложение на прием сообщений, отправляемых из iOS-приложения, создадим новый Swift-файл и назовем его ConnectionHelper.swift. Будем использовать этот класс для перехвата сообщений и событий, исходящих из iOS-приложения.
Для начала создадим класс ConnectionHelper и внутри него настроим сессию и события часов.
class ConnectionHelper: NSObject { var session: WCSession init(session: WCSession = .default) { self.session = session super.init() if WCSession.isSupported() { session.delegate = self session.activate() } } }
Этот код выдаст ошибку, что заставит нас реализовать необходимые методы в созданном классе для делегата сессии часов.
Чтобы сохранить чистоту кода, создайте расширение класса ConnectionHelper и реализуйте в нем WCSessionDelegate. Расширение также должно реализовать метод, показанный ниже, поскольку это необходимый метод.
Информация: расширения в Swift позволяют добавить новую функциональность в существующий класс.
extension ConnectionHelper: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } }
Этот метод необходим для индикации изменений в состоянии активации WCSession. Он позволяет увидеть, что связь между iOS-приложением и сопряженными с ним часами Apple Watch активна. В противном случае отправляется ошибка.
Следующим шагом будет получение событий и сообщений, отправленных из iOS-приложения в watchOS-приложение, для чего реализуем метод didReceiveMessage в расширении. Теперь расширение ConnectionHelper должно выглядеть следующим образом:
extension ConnectionHelper: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {} func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { } }
С помощью объявленного выше метода обновим значение счетчика. Сначала объявим его внутри класса ConnectionHelper. Теперь этот класс должен выглядеть следующим образом:
class ConnectionHelper: NSObject, ObservableObject { @Published var count = 0 var session: WCSession init(session: WCSession = .default) { self.session = session super.init() if WCSession.isSupported() { session.delegate = self session.activate() } } }
Мы объявили переменную count, аннотировали ее как Published и реализовали протокол ObservableObject в классе, чтобы отслеживать изменение переменной count.
Теперь нужно реализовать логику, которая будет обновлять значение count каждый раз при получении события от iOS-приложения. Для этого воспользуемся методом didReceiveMessage, определенным в расширении ConnectionHelper.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { guard let newCount = message["count"] as? Int else { return } DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.count = newCount replyHandler(["success": true, "count": count]) } }
Информация: при получении сообщения в watchOS-приложение из приложения iOS можно отправить ответ, используя обратный вызов replyHandler.
Теперь пришло время использовать созданную переменную счетчика в UI watchOS-приложения. Инстанцируем класс ConnectionHelper в ContentView и используем свойство count для отображения его под текстовым элементом.
struct ContentView: View { @ObservedObject var connectionHelper = ConnectionHelper() var body: some View { VStack { Text("\(connectionHelper.count)") } } }
Односторонняя связь между приложением iOS и приложением watchOS завершена.
Отправка сообщений из приложения watchOS в приложение iOS
- watchOS-приложение в качестве отправителя
Чтобы реализовать отправку сообщений из watchOS-приложения в iOS-приложение, сначала создадим вспомогательную функцию внутри класса ConnectionHelper. Эта функция будет отправлять переменную count в виде сообщения из watchOS- в iOS-приложение, используя созданную ранее сессию часов.
func sendNewCount() { let messageData = ["count": self.count] self.session.sendMessage(messageData) { reply in print("received response :", reply) } errorHandler: { error in print("error sending message :", error) } }
На следующем этапе модифицируем файл ContentView, чтобы добавить кнопки с текстом + и -, а затем добавим к ним действия. Как показано в приведенном ниже фрагменте кода, изменяем переменную count и используем функцию sendNewCount для отправки ее в iOS-приложение.
var body: some View { VStack { Button("+", action: { connectionHelper.count += 1 connectionHelper.sendNewCount() }) Text("\(connectionHelper.count)") Button("-", action: { connectionHelper.count -= 1 connectionHelper.sendNewCount() }) } }
Теперь осталось настроить iOS-приложение, чтобы оно прослушивало сообщения, отправленные из watchOS-приложения.
- iOS-приложение в качестве приемника
Чтобы обрабатывать входящие сообщения, сначала нужно использовать watchEvents для прослушивания сообщений; они импортируются из пакета react-native-watch-connectivity.
import { ... watchEvents, ... } from 'react-native-watch-connectivity'; ... const [count, setCount] = useState(0); useEffect(() => { watchEvents.on("message", (message) => { const newCount = message.count as number; if (newCount !== null && newCount !== undefined) { setCount(newCount); } }); }, []);
Вот теперь все. Запустим оба приложения и попробуем их в деле.
Добавление виджетов в приложение watchOS
Настройка таргета Widget Extension
Чтобы внедрить поддержку виджетов в watchOS-приложение, нужно создать новый таргет в том же проекте Xcode.
Добавим в проект новый таргет под названием Widget Extension (расширение виджета), аналогичный таргету watchOS.
После нажатия кнопки “Next” (“Далее”) надо указать название продукта для таргета-виджета.
После установки таргета Widget Extension в проекте появится новая папка, названная в данном случае CounterWidget. Изначально эта папка будет содержать два Swift-файла: AppIntent и CounterWidget.
Разработка и реализация UI виджета и его функциональности
Прежде чем модифицировать UI, необходимо настроить переменную count в Widget Extension.
Для этого откройте файл AppIntent, объявите переменную currentCount и настройте конструкторы, как показано в приведенном ниже фрагменте кода:
import AppIntents struct ConfigurationAppIntent: WidgetConfigurationIntent { ... var currentCount = 0 init() {} init(currentCount: Int = 0) { self.currentCount = currentCount } }
Чтобы начать разработку UI виджета, откройте файл CounterWidget. Прокрутите вниз до EntryView, который был автоматически сгенерирован Xcode при создании Widget Extension. Создадим очень простой текстовый компонент и внутри него отобразим текущую переменную count, как показано ниже:
struct CounterWidgetEntryView: View { var entry: Provider.Entry var body: some View { Text("Count: \(entry.configuration.currentCount)").multilineTextAlignment(.center) } }
Обмен данными между watchOS-приложением и Widget Extension
Теперь нужно отобразить исходную переменную count, объявленную в watchOS-приложении в UI виджета. Но Apple не предоставляет прямого способа обмена данными между watchOS-приложением и виджетом. Поэтому будем хранить переменную count в UserDefaults (это локальное хранилище, предоставляемое Apple).
Подробнее о UserDefaults можно почитать здесь.
Чтобы разделить одно свойство между watchOS-приложением и Widget Extension, нужно добавить новую возможность (capability) под названием App Groups как для watchOS, так и для Widget Extension.
Для этого откройте Project Settings -> выберите таргет, а затем выберите вкладку Signing & Capabilities. Нажмите кнопку + Capability для своего таргета в этом окне.
После добавления App Groups в качестве новой возможности вы увидите этот раздел в показанном ниже окне. Нажмите кнопку + и добавьте идентификатор.
Теперь укажите валидный идентификатор контейнера (который также будет использоваться в качестве имени набора для UserDefaults), где будут храниться данные, используемые несколькими таргетами в проекте.
Примечание: обязательно выполните этот шаг как для watchOS-приложения, так и для Widget Extension.
Файл CounterWidget будет содержать всю логику, управляющую пользовательским интерфейсом (UI) и его функциональностью. Однако главной функцией является функция timeline, которая отвечает за обновление UI виджета через заданные промежутки времени (подробную информацию можно найти здесь).
Теперь настроим механизм обмена данными между watchOS-приложением и Widget Extension.
Объявим новую переменную appCount в классе ConnectionHelper, который объявлен в папке таргета watchOS, и аннотируем ее с помощью AppStorage, используя UserDefaults, как показано ниже.
Кроме того, чтобы сделать переменную appCount функциональной, будем присваивать ей новое значение каждый раз, когда count изменяется.
class ConnectionHelper: NSObject, ObservableObject {
@AppStorage("appCount", store: UserDefaults(suiteName: "group.watchWidget.count")) var appCount = 0
@Published var count = 0 {
didSet {
appCount = count;
WidgetCenter.shared.reloadAllTimelines()
}
}
// ... Остальной код
}
На следующем этапе, чтобы отобразить ее в пользовательском интерфейсе виджета, нам нужно изменить Timeline Provider виджета. Для этого перейдем к предварительно сгенерированной структуре Provider в файле CounterWidget.
Прежде всего, необходимо объявить переменную appCount в области видимости этой структуры и изменить функции провайдера, чтобы они выглядели так, как показано ниже:
struct Provider: AppIntentTimelineProvider {
// Добавьте следующую строку, чтобы использовать переменную appCount, хранящуюся в UserDefaults:
@AppStorage("appCount", store: UserDefaults(suiteName: "group.watchWidget.count")) var appCount = 0
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(currentCount: appCount))
}
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
var config = configuration
config.currentCount = appCount
return SimpleEntry(date: Date(), configuration: config)
}
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var config = configuration
config.currentCount = appCount
let entry = SimpleEntry(date: Date(), configuration: config)
return Timeline(entries: [entry], policy: .atEnd)
}
func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] {
// Создайте массив со всеми предварительно настроенными виджетами для показа.
[AppIntentRecommendation(intent: ConfigurationAppIntent(currentCount: appCount), description: "Example Widget")]
}
}
Мы успешно настроили Widget Extension приложения watchOS для отображения значения счетчика. Теперь запустим его и опробуем.
Попробуем изменить значение счетчика из приложения iOS.
Полная версия проекта доступна в этом репозитории GitHub.
Заключение
Мы успешно интегрировали приложение на базе React Native с watchOS, а также добавили поддержку виджета для циферблатов часов.
Теперь ваша очередь создавать потрясающие приложения с помощью WatchKit и WidgetKit. Удачи!
Читайте также:
- Frontend Masters: принципы SOLID в React/React Native
- Дизайн системы для Чайников. Создаём стиль для приложения на React Native за 3 простых шага
- Стратегии рендеринга, которые должен знать каждый React-разработчик
Читайте нас в Telegram, VK и Дзен
Перевод статьи Kunjal Soni: React Native App with Apple Watch & Widget Support