Мы все любим разрабатывать мобильные приложения с помощью 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-приложении.

Укажите имя приложения и идентификатор пакета для приложения 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);
    }
  });
}, []);

Вот теперь все. Запустим оба приложения и попробуем их в деле.

Связь между приложениями iOS и watchOS

Добавление виджетов в приложение watchOS

Настройка таргета Widget Extension

Чтобы внедрить поддержку виджетов в watchOS-приложение, нужно создать новый таргет в том же проекте Xcode.

Добавим в проект новый таргет под названием Widget Extension (расширение виджета), аналогичный таргету watchOS.

Добавление таргета Widget Extension

После нажатия кнопки “Next” (“Далее”) надо указать название продукта для таргета-виджета.

Предоставление детальной информации о таргете Widget Extension

После установки таргета 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 в качестве новой возможности вы увидите этот раздел в показанном ниже окне. Нажмите кнопку + и добавьте идентификатор.

Добавление App Groups

Теперь укажите валидный идентификатор контейнера (который также будет использоваться в качестве имени набора для UserDefaults), где будут храниться данные, используемые несколькими таргетами в проекте.

Предоставьте имя контейнера App Groups

Примечание: обязательно выполните этот шаг как для watchOS-приложения, так и для Widget Extension.

Добавление новой возможности App Groups для таргетов 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 для отображения значения счетчика. Теперь запустим его и опробуем.

Обновление пользовательского интерфейса виджета при изменении счетчика из приложения watchOS

Попробуем изменить значение счетчика из приложения iOS.

Обновление пользовательского интерфейса виджета при изменении счетчика из iOS- или watchOS-приложения

Полная версия проекта доступна в этом репозитории GitHub

Заключение

Мы успешно интегрировали приложение на базе React Native с watchOS, а также добавили поддержку виджета для циферблатов часов.

Теперь ваша очередь создавать потрясающие приложения с помощью WatchKit и WidgetKit. Удачи!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kunjal Soni: React Native App with Apple Watch & Widget Support

Предыдущая статьяКак я создал 2D-игру с помощью Ebiten за 40 минут
Следующая статьяСоздание снэкбара с обратным отсчетом времени в Android с помощью Jetpack Compose