Noveo

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

React Native with Apple Watch

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

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-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.

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

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

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