Перейти к содержанию

Связь между iOS и React Native

В руководстве Интеграция с существующими приложениями и руководстве Native UI Components Guide мы узнаем, как внедрить React Native в нативный компонент и наоборот. Когда мы смешиваем нативные и React Native компоненты, мы в конечном итоге обнаружим необходимость в коммуникации между этими двумя мирами. Некоторые способы достижения этой цели уже упоминались в других руководствах. В этой статье мы обобщим имеющиеся методы.

Введение

React Native вдохновлен React, поэтому основная идея информационного потока схожа. Поток в React является однонаправленным. Мы поддерживаем иерархию компонентов, в которой каждый компонент зависит только от своего родителя и собственного внутреннего состояния. Мы делаем это с помощью свойств: данные передаются от родителя к его дочерним компонентам по принципу "сверху вниз". Если компонент-предок зависит от состояния своего потомка, необходимо передать обратный вызов, который будет использоваться потомком для обновления предка.

Та же концепция применима и к React Native. Пока мы создаем наше приложение исключительно в рамках фреймворка, мы можем управлять нашим приложением с помощью свойств и обратных вызовов. Но когда мы смешиваем React Native и нативные компоненты, нам нужны некоторые специфические, кросс-языковые механизмы, которые позволят нам передавать информацию между ними.

Свойства

Свойства — это самый простой способ межкомпонентного взаимодействия. Поэтому нам нужен способ передачи свойств как из native в React Native, так и из React Native в native.

Передача свойств из native в React Native

Для того чтобы встроить представление React Native в нативный компонент, мы используем RCTRootView. RCTRootView — это UIView, который содержит приложение React Native. Он также обеспечивает интерфейс между нативной стороной и размещенным приложением.

У RCTRootView есть инициализатор, который позволяет передавать произвольные свойства в приложение React Native. Параметр initialProperties должен быть экземпляром NSDictionary. Словарь внутренне преобразуется в JSON-объект, на который может ссылаться JS-компонент верхнего уровня.

1
2
3
4
5
6
7
8
NSArray *imageList = @[@"http://foo.com/bar1.png",
                       @"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                 moduleName:@"ImageBrowserApp"
                                          initialProperties:props];
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import React from 'react';
import { View, Image } from 'react-native';

export default class ImageBrowserApp extends React.Component {
    renderImage(imgURI) {
        return <Image source={{ uri: imgURI }} />;
    }
    render() {
        return (
            <View>
                {this.props.images.map(this.renderImage)}
            </View>
        );
    }
}

RCTRootView также предоставляет свойство чтения-записи appProperties. После установки appProperties приложение React Native перерисовывается с новыми свойствами. Обновление выполняется только тогда, когда новые обновленные свойства отличаются от предыдущих.

1
2
3
4
NSArray *imageList = @[@"http://foo.com/bar3.png",
                       @"http://foo.com/bar4.png"];

rootView.appProperties = @{@"images" : imageList};

Обновлять свойства можно в любое время. Однако обновления должны выполняться в главном потоке. Вы можете использовать getter в любом потоке.

В настоящее время известна проблема, когда при установке appProperties во время запуска моста изменения могут быть потеряны. См. обсуждение для получения дополнительной информации.

Не существует способа обновлять только несколько свойств за раз. Мы предлагаем вам встроить это в вашу собственную обертку.

Передача свойств из React Native в native

Проблема раскрытия свойств нативных компонентов подробно рассмотрена в этой статье. Вкратце, экспортируйте свойства с помощью макроса RCT_CUSTOM_VIEW_PROPERTY в ваш пользовательский нативный компонент, а затем используйте их в React Native, как если бы компонент был обычным React Native компонентом.

Ограничения свойств

Основной недостаток кросс-языковых свойств заключается в том, что они не поддерживают обратные вызовы, которые позволили бы нам работать с привязкой данных снизу вверх. Представьте, что у вас есть небольшое представление RN, которое вы хотите удалить из родительского представления в результате действия JS. Это невозможно сделать с помощью пропсов, так как информация должна передаваться снизу вверх.

Хотя у нас есть возможность использовать межъязыковые обратные вызовы (описано здесь), эти обратные вызовы не всегда то, что нам нужно. Основная проблема заключается в том, что они не предназначены для передачи в качестве свойств. Скорее, этот механизм позволяет нам вызвать нативное действие из JS и обработать результат этого действия в JS.

Другие способы межъязыкового взаимодействия (события и нативные модули)

Как уже говорилось в предыдущей главе, использование свойств связано с некоторыми ограничениями. Иногда свойств недостаточно для управления логикой нашего приложения, и нам нужно решение, обеспечивающее большую гибкость. В этой главе рассматриваются другие методы коммуникации, доступные в React Native. Они могут быть использованы как для внутренней связи (между JS и нативными слоями в RN), так и для внешней связи (между RN и "чистой нативной" частью вашего приложения).

React Native позволяет выполнять кросс-языковые вызовы функций. Вы можете выполнять пользовательский нативный код из JS и наоборот. К сожалению, в зависимости от того, на какой стороне мы работаем, мы достигаем одной и той же цели разными способами. Для native — мы используем механизм событий для планирования выполнения функции-обработчика в JS, а для React Native мы напрямую вызываем методы, экспортируемые нативными модулями.

Вызов функций React Native из native (события)

События подробно описаны в этой статье. Обратите внимание, что использование событий не дает нам никаких гарантий относительно времени выполнения, поскольку событие обрабатывается в отдельном потоке.

События являются мощным инструментом, поскольку они позволяют нам изменять компоненты React Native без необходимости ссылки на них. Однако есть несколько подводных камней, на которые можно нарваться при их использовании:

  • Поскольку события могут быть отправлены откуда угодно, они могут привнести в ваш проект зависимости в стиле "спагетти".
  • События разделяют пространство имен, что означает, что вы можете столкнуться с некоторыми коллизиями имен. Коллизии не будут обнаружены статически, что затрудняет их отладку.
  • Если вы используете несколько экземпляров одного и того же компонента React Native и хотите различать их с точки зрения события, вам, скорее всего, придется ввести идентификаторы и передавать их вместе с событиями (в качестве идентификатора можно использовать reactTag нативного представления).

Общий шаблон, который мы используем при встраивании native в React Native, заключается в том, чтобы сделать RCTViewManager нативного компонента делегатом для представлений, отправляя события обратно в JavaScript через мост. Это позволяет держать связанные вызовы событий в одном месте.

Вызов нативных функций из React Native (нативные модули)

Нативные модули — это классы Objective-C, которые доступны в JS. Как правило, на один JS-мост создается один экземпляр каждого модуля. Они могут экспортировать произвольные функции и константы в React Native. Они были подробно рассмотрены в этой статье.

Тот факт, что нативные модули являются синглтонами, ограничивает механизм в контексте встраивания. Допустим, у нас есть компонент React Native, встроенный в нативное представление, и мы хотим обновить нативное, родительское представление. Используя механизм нативного модуля, мы экспортируем функцию, которая принимает не только ожидаемые аргументы, но и идентификатор родительского нативного представления. Идентификатор будет использоваться для получения ссылки на родительское представление для обновления. При этом нам потребуется сохранить в модуле связку между идентификаторами и родительскими представлениями.

Хотя это решение является сложным, оно используется в RCTUIManager, который является внутренним классом React Native, управляющим всеми представлениями React Native.

Нативные модули также могут быть использованы для раскрытия существующих нативных библиотек в JS. Библиотека Geolocation library является живым примером этой идеи.

Все нативные модули используют одно и то же пространство имен. Остерегайтесь коллизий имен при создании новых модулей.

Поток вычислений в макете

При интеграции native и React Native нам также необходим способ консолидации двух различных систем верстки. В этом разделе рассматриваются распространенные проблемы верстки и дается краткое описание механизмов их решения.

Макет нативного компонента, встроенного в React Native

Этот случай рассматривается в этой статье. Подведем итог: поскольку все наши нативные представления react являются подклассами UIView, большинство атрибутов стиля и размера будут работать так, как вы ожидаете из коробки.

Макет компонента React Native, встроенного в native

React Native контент с фиксированным размером

Общий сценарий — это когда у нас есть приложение React Native с фиксированным размером, который известен нативной стороне. В частности, полноэкранное представление React Native попадает в этот случай. Если нам нужно корневое представление меньшего размера, мы можем явно задать рамку RCTRootView.

Например, чтобы сделать приложение RN высотой 200 (логических) пикселей, а ширину хостингового представления — широкой, мы можем сделать следующее:

SomeViewController.m
1
2
3
4
5
6
7
8
9
- (void)viewDidLoad
{
  [...]
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:appName
                                            initialProperties:props];
  rootView.frame = CGRectMake(0, 0, self.view.width, 200);
  [self.view addSubview:rootView];
}

Когда у нас есть корневое представление фиксированного размера, нам необходимо соблюдать его границы на стороне JS. Другими словами, нам нужно убедиться, что содержимое React Native может быть заключено в рамки корневого представления фиксированного размера. Самый простой способ обеспечить это — использовать Flexbox-макет. Если вы используете абсолютное позиционирование, и компоненты React будут видны за пределами корневого представления, вы получите перекрытие с нативными представлениями, что приведет к неожиданному поведению некоторых функций. Например, 'TouchableHighlight' не будет подсвечивать ваши прикосновения вне границ корневого представления.

Совершенно нормально обновлять размер корневого представления динамически, переустанавливая его свойство frame. React Native позаботится о компоновке контента.

Контент React Native с гибкими размерами

В некоторых случаях мы хотим отображать контент изначально неизвестного размера. Допустим, размер будет определяться динамически в JS. У нас есть два решения этой проблемы.

  1. Вы можете обернуть ваше представление React Native в компонент ScrollView. Это гарантирует, что ваш контент всегда будет доступен и не будет перекрываться нативными представлениями.
  2. React Native позволяет определить в JS размер приложения RN и предоставить его владельцу хостинга RCTRootView. Владелец затем отвечает за повторную компоновку вложенных представлений и сохранение единообразия пользовательского интерфейса. Мы достигаем этого с помощью режимов гибкости RCTRootView.

RCTRootView поддерживает 4 различных режима гибкости размера:

RCTRootView.h
1
2
3
4
5
6
typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
  RCTRootViewSizeFlexibilityNone = 0,
  RCTRootViewSizeFlexibilityWidth,
  RCTRootViewSizeFlexibilityHeight,
  RCTRootViewSizeFlexibilityWidthAndHeight,
};

RCTRootViewSizeFlexibilityNone — это значение по умолчанию, которое делает размер корневого представления фиксированным (но его все еще можно обновлять с помощью setFrame:). Три других режима позволяют нам отслеживать обновления размеров содержимого React Native. Например, установка режима RCTRootViewSizeFlexibilityHeight заставит React Native измерить высоту контента и передать эту информацию делегату RCTRootView. В делегате можно выполнить произвольное действие, включая установку рамки корневого представления, чтобы содержимое поместилось. Делегат вызывается только тогда, когда размер содержимого изменился.

Если сделать размер гибким как в JS, так и в native, это приведет к неопределенному поведению. Например — не делайте ширину React компонента верхнего уровня гибкой (с помощью flexbox), в то время как вы используете RCTRootViewSizeFlexibilityWidth на хостинге RCTRootView.

Давайте рассмотрим пример.

FlexibleSizeExampleView.m
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
- (instancetype)initWithFrame:(CGRect)frame
{
  [...]

  _rootView = [[RCTRootView alloc] initWithBridge:bridge
  moduleName:@"FlexibilityExampleApp"
  initialProperties:@{}];

  _rootView.delegate = self;
  _rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
  _rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
  CGRect newFrame = rootView.frame;
  newFrame.size = rootView.intrinsicContentSize;

  rootView.frame = newFrame;
}

В примере у нас есть представление FlexibleSizeExampleView, которое содержит корневое представление. Мы создаем корневое представление, инициализируем его и устанавливаем делегата. Делегат будет обрабатывать обновления размеров. Затем мы устанавливаем гибкость размера корневого представления в RCTRootViewSizeFlexibilityHeight, что означает, что метод rootViewDidChangeIntrinsicSize: будет вызываться каждый раз, когда содержимое React Native изменит свою высоту. Наконец, мы задаем ширину и положение корневого представления. Обратите внимание, что мы также установили высоту, но это не имеет никакого эффекта, так как мы сделали высоту RN-зависимой.

Полный исходный код примера вы можете посмотреть здесь.

Можно динамически изменять режим гибкости размера корневого представления. Изменение режима гибкости корневого представления запланирует пересчет макета, и метод делегата rootViewDidChangeIntrinsicSize: будет вызван, как только станет известен размер содержимого.

Расчет макета в React Native выполняется в отдельном потоке, в то время как обновление представления пользовательского интерфейса выполняется в основном потоке.

Это может привести к временным несоответствиям пользовательского интерфейса между нативным и React Native. Это известная проблема, и наша команда работает над синхронизацией обновлений пользовательского интерфейса, поступающих из разных источников.

React Native не выполняет никаких вычислений макета до тех пор, пока корневое представление не станет подпредставлением некоторых других представлений.

Если вы хотите скрыть представление React Native до тех пор, пока его размеры не станут известны, добавьте корневое представление в качестве вложенного представления и сделайте его изначально скрытым (используйте свойство hidden у UIView). Затем измените его видимость в методе делегата.

Комментарии