Noveo

Наш блог React Native with Apple Watch

React Native with Apple Watch

Разработчик Noveo Андрей рассказывает о своем эксперименте программирования на React Native для Apple Watch.

 

Я программирую на React Native уже около трех лет. Не так давно я стал обладателем Apple Watch и, конечно, сразу решил разобраться в том, что я сам могу разработать для них. Похоже, что не так много людей решили то же самое :) Поэтому дальше я расскажу о своем опыте разработки приложения для Apple Watch с мобильным приложением на React Native.

React Native with Apple Watch

Hello world: начало…

Я не буду описывать, как сделал свое первое приложение. Главное, что у меня получилось достаточно быстро написать читаемый код для общения RN app на Typescript с AW app на Swift. Итак, первая строка «Hello world» отправлена на часы. Пора разобраться, как это работает.

 

Есть единственная нормальная библиотека для работы с часами — react-native-watch-connectivity. «Из коробки» RN ничего не умеет. Разработчики React Native сосредоточены только на iOS/Android. Я прочитал об этом несколько небольших старых статей (некоторые были написаны еще в конце 2015 года). Здесь уточню, что react-native-watch-connectivity обновлялась в конце 2020 года.

 

У библиотеки есть неплохая документация, а на github.com/mtford90/react-native-watch-connectivity есть достаточно перегруженный пример, в котором, пока не разобрался, все кажется черной магией. Где и как там слушаются сообщения от часов, непонятно даже после прочтения доки. Информации в сети по RN с AW почти нет, как нет и описания лучших практик. В связи с этим я решил совсем не оглядываться на код из примера и написать все так, как хочется мне. Получился понятный и читаемый код, о котором я сейчас расскажу подробнее — как и о первом подводном камне.

Apple Watch app

Во-первых, приложение для часов пишем только на Swift. Позже я объясню, почему это норма для кроссплатформенной разработки.

 

Итак, открываем проект в Xcode и добавляем новые targets для часов. После нужно заполнить только name нашего AW app и проверить, что выбраны interface storyboard и language Swift. Обязательно активируем новую scheme для часов. Xcode сгенерировал нам две папки WAppName и WAppName Extension.

 

Здесь нас ожидает первый подводный камень — создать бридж для Objective-C. В основной папке проекта создаем пустой файл .swift и обязательно создаем Bridging Header. В нашем примере часы будут получать все данные от телефона. Можем, конечно, получать данные и из сети, но тогда нам нужно будет написать свою нативную логику в контроллерах. Есть, например, NotificationController. Но сегодня мы говорим о RN, а не о нативных модулях.

 

При создании новых таргетов для часов в проекте создается и базовая верстка в Interface.storyboard. Удаляем ненужное и добавляем необходимые компоненты по нажатию на + справа от статусной панели в Xcode. Я добавил три Button’s, указава для каждого Title. Стоит уточнить, что верстка на AW имеет свои ограничения по размещению компонентов, но все интуитивно понятно в панели справа. Над макетом часов имеется желтая кнопка Interface Controller, указывающая на контроллер, в котором будет описана логика поведения. У этой кнопки есть удобная опция добавления нужных функций (в нашем случае onColorPressed) для наших Button’s — просто перетягиваем от нее линию в открытый рядом файл InterfaceController.swift.

 

В InterfaceController уже описана часть логики. Например, session. Также обязательно добавляем импорты WatchKit, Foundation и WatchConnectivity.

class InterfaceController: WKInterfaceController, WCSessionDelegate {
 @IBOutlet weak var greenButton: WKInterfaceButton!
 @IBOutlet weak var yellowButton: WKInterfaceButton!
 @IBOutlet weak var redButton: WKInterfaceButton!
  func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
  func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
   print("watch received message", message);
   let currentLight = message["text"] as! String
   let title = message["title"] as! String
   setCurrentLight(currentLight: currentLight, title: title)
 }
 var session: WCSession?
  override func awake(withContext context: Any?) {
   super.awake(withContext: context)
  
   if WCSession.isSupported() {
     self.session = WCSession.default
     self.session?.delegate = self
     self.session?.activate()
   }




//...
 func setCurrentLight(currentLight: String, title: String) {
   resetButtonColors()
  
   switch currentLight {
   case "green":
     greenButton.setBackgroundColor(UIColor(red: 33/255, green: 150/255, blue: 243/255, alpha: 1))
   case "yellow":
     yellowButton.setBackgroundColor(UIColor(red: 62/255, green: 163/255, blue: 244/255, alpha: 1))
   case "red":
     redButton.setBackgroundColor(UIColor(red: 115/255, green: 185/255, blue: 241/255, alpha: 1))
   case "greenTitle":
     greenButton.setBackgroundColor(UIColor(red: 33/255, green: 150/255, blue: 243/255, alpha: 1))
     greenButton.setTitle(title)
   default:
     break
   }
 }

 func sendMessageToApp(currentLight: String, title: String) {
   resetButtonColors()
   setCurrentLight(currentLight: currentLight, title: title)
   print("Sending response")
   session?.sendMessage(["message": currentLight], replyHandler: { (dict) in
     print("Received response")
   }, errorHandler: nil)
 }
  func resetButtonColors() {
   greenButton.setBackgroundColor(UIColor.white)
   yellowButton.setBackgroundColor(UIColor.white)
   redButton.setBackgroundColor(UIColor.white)
   greenButton.setTitle("100 ml")
 }
 @IBAction func onGreenPressed() {
   sendMessageToApp(currentLight: "green", title: "100 ml")
 }
 @IBAction func onYellowPressed() {
   sendMessageToApp(currentLight:"yellow", title: "50 ml")
 }
 @IBAction func onRedPressed() {
   sendMessageToApp(currentLight:"red", title: "30 ml")
 }

Определяем нужные нам переменные. Ниже описываем нужные нам функции, в том числе вызываем их в onColorPressed-функции.

 

Похоже, что в этой части во всем разобрались. Едем дальше.

React Native app

Пришло время упомянуть, что это приложение для контроля за гидратацией организма, пока только для достижения ежедневной цели — выпивать суточную норму 2 литра воды. Я остановлюсь только на экране, где описана вся логика. Из react-native-watch-connectivity нам потребуется импортировать только sendMessage, getIsPaired, watchEvents.

const sendMessageToAppleWatch = (text: string, title: string) => {
 sendMessage({text, title}, error => {
   if (error) console.log(error);
 });
};

export const GlassScreen = () => {
 const [fullnessGlass, setFullnessGlass] = useState(fullness);
 const [isPaired, setPaired] = useState<Paired>(false);
 const [inputText, setInputText] = useState<string>('');

 const paired = getIsPaired();
 const unsubscribe = watchEvents.on('message', (message: MessageFromAW) => {
   console.log('received message from watch', message);
   setCurrentFullness(message.message);
 });

 const onChangeText = (text: string) => {
   setInputText(text);
 };

 useEffect(() => {
   setPaired(paired);
 }, []);

 useEffect(() => {
   unsubscribe;
 }, []);

 const onButtonPressed = (id: string, label: string) => {
   setCurrentFullness(id);
   sendMessageToAppleWatch(id, label);
 };

 const onChangingTitlePressed = (text: string) => {
   const checkText = text.length ? `${text} ml` : '100 ml';
   setCurrentFullness('green', checkText);
   sendMessageToAppleWatch('greenTitle', checkText);
 };


 const setCurrentFullness = (currentId: string, title?: string) => {
   const newFullnessGlass = fullnessGlass.map(
     ({id, color, isSelected, label}) => {
       return {
         id,
         color,
         label: (id === currentId && title) || label,
         isSelected: id === currentId ? (isSelected = true) : (isSelected = false),
       };
     },
   );

   setFullnessGlass(newFullnessGlass);
 };
 return (
   <SafeAreaView>
     <PairingIndicator isPaired={isPaired} />
     <HeaderLabel />
     <GlassButtons
       fullnessGlass={fullnessGlass}
       onButtonPressed={onButtonPressed}
     />
     <ChangeButton
       inputText={inputText}
       onChangeText={onChangeText}
       onButtonPressed={onChangingTitlePressed}
     />
   </SafeAreaView>
 );
};

Две основных функции для общения с часами — sendMessageToAppleWatch и unsubscribe. Также я добавил индикатор пейринга — просто для демонстрации возможностей. С полным списком можно ознакомиться в документации http://mtford.co.uk/react-native-watch-connectivity/.

React Native with Apple Watch

Выводы

Сначала остановимся на минусах. Разработчики react-native-watch-connectivity в документации сами пишут о проблемах с интеграцией и откликом на симуляторе. Они настоятельно рекомендуют вместо этого использовать настоящие устройства. От себя добавлю, что в Xcode нужно создать отдельный новый симулятор айфона с пейрингом к часам. По отдельности ранее созданные симуляторы не заработают. Иногда я сталкивался и с описанной разработчиками хитроумной «dodgy»-настройкой симулятора Watch/iOS от Apple. Или, например, с тем, что если продолжить переустанавливать каждое приложение, оно в конечном итоге будет работать.

 

В результате моего изучения взаимодействия RN с Apple Watch черная магия испарилась. Да, мы не имеем альтернативы react-native-watch-connectivity. Но имеем понятную документацию и работоспособные инструменты для разработки. При их помощи есть возможность реализовать достаточно сложную функциональность, оставляя всю привычную логику стейт-менеджмента и взаимодействия с бэкендом на стороне RN.

 

Для большей убежденности в том, что плюсы перевешивают минусы, а разработка Apple Watch app невозможна без программирования на Swift, я обратился к альтернативному кроссплатформенному фреймворку. Есть мнение, и не только мое, что RN не развивается. Я разобрался, как с Apple Watch работает прогрессивный Flutter. Бонусом добавлю, что Flutter из коробки умеет отправлять message на часы.

 

import ‘package:flutter/services.dart’;

static const channel = const MethodChannel(‘myWatchChannel’);

channel.invokeMethod(«sendStringToNative», _counter.toString());

 

А часть Apple Watch app очень похожа на то, что я описывал выше. Только ему нужна дополнительная настройка session в AppDelegate.swift.

Получается, что изученный и описанный мной паттерн разработки приложений Apple Watch — единственно верный и устоявшийся стандарт для кроссплатформенной мобильной разработки.

 

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: