Нативные UI компоненты iOS¶
Native Module и Native Components — это наши стабильные технологии, используемые в унаследованной архитектуре. Они будут устаревшими в будущем, когда новая архитектура станет стабильной. Новая архитектура использует Turbo Native Module и Fabric Native Components для достижения аналогичных результатов.
Существует масса нативных виджетов пользовательского интерфейса, готовых к использованию в новейших приложениях — некоторые из них являются частью платформы, другие доступны в виде библиотек сторонних разработчиков, а еще больше могут использоваться в вашем собственном портфолио. В React Native уже обернуто несколько наиболее важных компонентов платформы, таких как ScrollView
и TextInput
, но не все из них, и уж точно не те, которые вы могли написать сами для предыдущего приложения. К счастью, мы можем обернуть эти существующие компоненты для бесшовной интеграции с вашим приложением React Native.
Как и руководство по нативным модулям, это более продвинутое руководство, которое предполагает, что вы немного знакомы с программированием для iOS. Это руководство покажет вам, как создать нативный компонент пользовательского интерфейса, проведя вас через реализацию подмножества существующего компонента MapView
, доступного в основной библиотеке React Native.
Пример iOS MapView¶
Допустим, мы хотим добавить интерактивную карту в наше приложение — с тем же успехом можно использовать MKMapView
, нам нужно только сделать ее пригодной для использования из JavaScript.
Нативные представления создаются и управляются подклассами RCTViewManager
. Эти подклассы похожи по функциям на контроллеры представлений, но по сути являются синглтонами — мост создает только один экземпляр каждого из них. Они открывают собственные представления для RCTUIManager
, который делегирует им обратную связь для установки и обновления свойств представлений по мере необходимости. Обычно RCTViewManager
также является делегатом для представлений, посылая события обратно в JavaScript через мост.
Чтобы открыть представление, вы можете:
- Подкласс
RCTViewManager
для создания менеджера для вашего компонента. - Добавить маркерный макрос
RCT_EXPORT_MODULE()
. - Реализовать метод
-(UIView *)view
.
RNTMapManager.m | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Не пытайтесь установить свойства frame
или backgroundColor
для экземпляра UIView
, который вы открываете через метод -view
.
React Native перезапишет значения, установленные вашим пользовательским классом, чтобы соответствовать пропсам компоновки вашего JavaScript-компонента.
Если вам нужна такая степень контроля, возможно, лучше обернуть экземпляр UIView
, который вы хотите стилизовать, в другой UIView
и вернуть оберточный UIView
вместо него.
Дополнительную информацию см. в Issue 2948.
В приведенном выше примере мы добавили к имени нашего класса префикс RNT
. Префиксы используются для того, чтобы избежать столкновения имен с другими фреймворками.
Фреймворки Apple используют двухбуквенные префиксы, а React Native использует в качестве префикса RCT
. Чтобы избежать коллизии имен, мы рекомендуем использовать в собственных классах трехбуквенный префикс, отличный от RCT
.
Затем вам понадобится немного JavaScript, чтобы сделать этот компонент пригодным для использования в React:
MapView.tsx | |
---|---|
1 2 3 4 |
|
MyApp.tsx | |
---|---|
1 2 3 4 5 6 7 |
|
Убедитесь, что здесь используется RNTMap
. Мы хотим потребовать здесь менеджера, который откроет представление нашего менеджера для использования в JavaScript.
При рендеринге не забудьте растянуть представление, иначе вы будете смотреть на пустой экран.
1 2 3 |
|
Теперь это полностью функционирующий компонент просмотра карты на JavaScript, с поддержкой pinch-zoom и других жестов. Однако мы пока не можем управлять им из JavaScript :(
Свойства¶
Первое, что мы можем сделать, чтобы сделать этот компонент более удобным для использования, это передать некоторые свойства. Допустим, мы хотим иметь возможность отключить масштабирование и указать видимую область. Отключение масштабирования — это булево значение, поэтому мы добавим одну строчку:
RNTMapManager.m | |
---|---|
1 |
|
Обратите внимание, что мы явно указываем тип как BOOL
— React Native использует RCTConvert
под капотом для преобразования всевозможных типов данных при общении через мост, и плохие значения будут показывать удобные ошибки "RedBox", чтобы дать вам знать о проблеме как можно скорее. Когда все так просто, о реализации за вас позаботится этот макрос.
Теперь, чтобы фактически отключить масштабирование, мы устанавливаем свойство в JS:
MyApp.tsx | |
---|---|
1 |
|
Чтобы документировать свойства (и какие значения они принимают) нашего компонента MapView, мы добавим компонент-обертку и задокументируем интерфейс с помощью React PropTypes
:
MapView.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Теперь у нас есть хорошо документированный компонент-обертка для работы.
Далее добавим более сложный пропс region
. Начнем с добавления собственного кода:
RNTMapManager.m | |
---|---|
1 2 3 4 |
|
Ок, это более сложный случай, чем BOOL
, который мы имели раньше. Теперь у нас есть тип MKCoordinateRegion
, которому нужна функция преобразования, и у нас есть пользовательский код, чтобы представление анимировалось, когда мы устанавливаем регион из JS. В теле функции, которое мы предоставляем, json
означает необработанное значение, которое было передано из JS. Есть также переменная view
, которая дает нам доступ к экземпляру представления менеджера, и defaultView
, которую мы используем для возврата свойства к значению по умолчанию, если JS посылает нам null sentinel.
Вы можете написать любую функцию преобразования, какую захотите, для вашего представления — вот реализация для MKCoordinateRegion
через категорию RCTConvert
. Она использует уже существующую категорию ReactNative RCTConvert+CoreLocation
:
RNTMapManager.m | |
---|---|
1 |
|
RCTConvert+Mapkit.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Эти функции преобразования предназначены для безопасной обработки любого JSON, который JS может бросить в них, отображая ошибки "RedBox" и возвращая стандартные значения инициализации, когда встречаются отсутствующие ключи или другие ошибки разработчика.
Чтобы завершить поддержку пропса region
, нам нужно задокументировать его в propTypes
:
MapView.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
MyApp.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Здесь вы можете видеть, что форма области явно указана в документации JS.
События¶
Итак, теперь у нас есть собственный компонент карты, которым мы можем свободно управлять из JS, но как нам быть с событиями от пользователя, такими как пинч-зум или панорамирование для изменения видимой области?
До сих пор мы только возвращали экземпляр MKMapView
из метода -(UIView *)view
нашего менеджера. Мы не можем добавить новые свойства в MKMapView
, поэтому нам придется создать новый подкласс от MKMapView
, который мы используем для нашего представления. Затем мы можем добавить обратный вызов onRegionChange
для этого подкласса:
RNTMapView.h | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
RNTMapView.m | |
---|---|
1 2 3 4 5 |
|
Обратите внимание, что все RCTBubblingEventBlock
должны иметь префикс on
. Далее объявите свойство обработчика событий на RNTMapManager
, сделайте его делегатом для всех представлений, которые он открывает, и передавайте события в JS, вызывая блок обработчика событий из родного представления.
RNTMapManager.m | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
В методе делегата -mapView:regionDidChangeAnimated:
вызывается блок обработчика события на соответствующем представлении с данными региона. Вызов блока обработчика события onRegionChange
приводит к вызову того же самого пропса обратного вызова в JavaScript. Этот обратный вызов вызывается с необработанным событием, которое мы обычно обрабатываем в компоненте-обертке для упрощения API:
MapView.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
MyApp.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Работа с несколькими нативными представлениями¶
Представление React Native может иметь более одного дочернего представления в дереве представлений, например.
1 2 3 4 5 |
|
В этом примере класс MyNativeView
является оберткой для NativeComponent
и раскрывает методы, которые будут вызываться на платформе iOS. Класс MyNativeView
определен в MyNativeView.ios.js
и содержит прокси-методы NativeComponent
.
Когда пользователь взаимодействует с компонентом, например, нажимает на кнопку, backgroundColor
MyNativeView
меняется. В этом случае UIManager
не будет знать, какой MyNativeView
должен быть обработан и какой из них должен изменить backgroundColor
. Ниже вы найдете решение этой проблемы:
1 2 3 4 5 6 7 8 9 |
|
Теперь вышеуказанный компонент имеет ссылку на определенный MyNativeView
, что позволяет нам использовать конкретный экземпляр MyNativeView
. Теперь кнопка может контролировать, какой MyNativeView
должен изменить свой backgroundColor
. В этом примере предположим, что callNativeMethod
изменяет backgroundColor
.
MyNativeView.ios.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
callNativeMethod
— это наш пользовательский метод iOS, который, например, изменяет backgroundColor
, отображаемый через MyNativeView
. Этот метод использует UIManager.dispatchViewManagerCommand
, который требует 3 параметра:
(nonnull NSNumber \*)reactTag
— id представления react.commandID:(NSInteger)commandID
— Id нативного метода, который должен быть вызванcommandArgs:(NSArray<id>\*)commandArgs
— Args нативного метода, которые мы можем передать из JS в native.
RNCMyNativeViewManager.m | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Здесь callNativeMethod
определен в файле RNCMyNativeViewManager.m
и содержит только один параметр, которым является (nonnull NSNumber*) reactTag
. Эта экспортируемая функция находит определенное представление с помощью addUIBlock
, который содержит параметр viewRegistry
, и возвращает компонент на основе reactTag
, позволяя вызвать метод на нужном компоненте.
Стили¶
Поскольку все наши родные представления react являются подклассами UIView
, большинство атрибутов стиля будут работать так, как вы ожидаете. Однако некоторые компоненты будут иметь стиль по умолчанию, например, UIDatePicker
, который имеет фиксированный размер. Этот стиль по умолчанию важен для того, чтобы алгоритм компоновки работал так, как ожидается, но мы также хотим иметь возможность переопределять стиль по умолчанию при использовании компонента. DatePickerIOS
делает это, оборачивая родной компонент в дополнительное представление, которое имеет гибкую стилизацию, и используя фиксированный стиль (который генерируется с помощью констант, передаваемых из родного компонента) для внутреннего родного компонента:
DatePickerIOS.ios.tsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Константы RCTDatePickerIOSConsts
экспортируются из native путем захвата фактического кадра native компонента следующим образом:
RCTDatePickerManager.m | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В этом руководстве рассмотрены многие аспекты создания мостов с использованием собственных нативных компонентов, но есть еще больше аспектов, которые вам, возможно, придется рассмотреть, например, пользовательские крючки для вставки и компоновки вложенных представлений. Если вы хотите углубиться еще больше, посмотрите исходный код некоторых реализованных компонентов.