Нативные модули iOS¶
Native Module и Native Components — это наши стабильные технологии, используемые в унаследованной архитектуре. Они будут устаревшими в будущем, когда новая архитектура станет стабильной. Новая архитектура использует Turbo Native Module и Fabric Native Components для достижения аналогичных результатов.
Добро пожаловать в раздел "Нативные модули для iOS". Пожалуйста, начните с чтения Native Modules Intro, чтобы узнать, что такое нативные модули.
Создание нативного модуля календаря¶
В следующем руководстве вы создадите нативный модуль CalendarModule
, который позволит вам получить доступ к API календаря Apple из JavaScript. В конце вы сможете вызывать CalendarModule.createCalendarEvent('Dinner Party', 'My House');
из JavaScript, вызывая нативный метод, который создает событие календаря.
Установка¶
Чтобы начать работу, откройте проект iOS в вашем приложении React Native в Xcode. Вы можете найти свой проект iOS в приложении React Native здесь:
Мы рекомендуем использовать Xcode для написания нативного кода. Xcode создан для разработки iOS, и его использование поможет вам быстро устранить мелкие ошибки, такие как синтаксис кода.
Создание файлов пользовательского нативного модуля¶
Первым шагом будет создание заголовка и файлов реализации нашего основного пользовательского нативного модуля. Создайте новый файл RCTCalendarModule.h
.
и добавьте к нему следующее:
1 2 3 4 |
|
Вы можете использовать любое имя, которое подходит для создаваемого вами нативного модуля. Назовите класс RCTCalendarModule
, поскольку вы создаете нативный модуль календаря. Поскольку в ObjC нет поддержки пространств имен на уровне языка, как в Java или C++, принято добавлять к имени класса подстроку. Это может быть аббревиатура имени вашего приложения или имени вашей инфры. RCT, в данном примере, означает React.
Как вы можете видеть ниже, класс CalendarModule реализует протокол RCTBridgeModule
. Нативный модуль — это класс Objective-C, который реализует протокол RCTBridgeModule
.
Далее приступим к реализации нативного модуля. Создайте соответствующий файл реализации, RCTCalendarModule.m
, в той же папке и включите в него следующее содержимое:
1 2 3 4 5 6 7 8 9 |
|
Имя модуля¶
Пока что ваш нативный модуль RCTCalendarModule.m
включает только макрос RCT_EXPORT_MODULE
, который экспортирует и регистрирует класс нативного модуля в React Native. Макрос RCT_EXPORT_MODULE
также принимает необязательный аргумент, который указывает имя, под которым модуль будет доступен в вашем JavaScript-коде.
Этот аргумент не является строковым литералом. В примере ниже передается RCT_EXPORT_MODULE(CalendarModuleFoo)
, а не RCT_EXPORT_MODULE("CalendarModuleFoo")
.
1 2 |
|
Затем к нативному модулю можно получить доступ в JS следующим образом:
1 |
|
Если вы не укажете имя, имя модуля JavaScript будет соответствовать имени класса Objective-C, с удаленными префиксами "RCT" или "RK".
Давайте последуем приведенному ниже примеру и вызовем RCT_EXPORT_MODULE
без каких-либо аргументов. В результате модуль будет открыт для React Native под именем CalendarModule
, так как это имя класса Objective-C с удаленным RCT.
1 2 |
|
Затем к нативному модулю можно получить доступ в JS следующим образом:
1 |
|
Экспорт нативного метода в JavaScript¶
React Native не будет экспортировать методы нативного модуля в JavaScript, если это не указано явно. Это можно сделать с помощью макроса RCT_EXPORT_METHOD
. Методы, написанные в макросе RCT_EXPORT_METHOD
, являются асинхронными, поэтому тип возврата всегда void. Чтобы передать результат от метода RCT_EXPORT_METHOD
в JavaScript, вы можете использовать обратные вызовы или эмитировать события (об этом ниже). Давайте продолжим и создадим нативный метод для нашего нативного модуля CalendarModule
с помощью макроса RCT_EXPORT_METHOD
. Назовем его createCalendarEvent()
и пока что пусть он принимает аргументы name и location в виде строк. Опции типа аргументов будут рассмотрены в ближайшее время.
1 2 3 |
|
Обратите внимание, что макрос
RCT_EXPORT_METHOD
не нужен в TurboModules, если только ваш метод не использует преобразование аргументов RCT (см. типы аргументов ниже). В конечном итоге React Native удалитRCT_EXPORT_MACRO,
поэтому мы не рекомендуем использоватьRCTConvert
. Вместо этого вы можете выполнить преобразование аргументов в теле метода.
Прежде чем вы создадите функциональность метода createCalendarEvent()
, добавьте консольный лог в метод, чтобы вы могли подтвердить, что он был вызван из JavaScript в вашем приложении React Native. Используйте API RCTLog
из React. Давайте импортируем этот заголовок в верхней части вашего файла, а затем добавим вызов журнала.
1 2 3 4 5 |
|
Синхронные методы¶
Вы можете использовать RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD
для создания синхронного нативного метода.
1 2 3 4 |
|
Возвращаемый тип этого метода должен быть объектного типа (id) и должен быть сериализуемым в JSON. Это означает, что хук может возвращать только значения nil или JSON (например, NSNumber, NSString, NSArray, NSDictionary).
На данный момент мы не рекомендуем использовать синхронные методы, поскольку синхронный вызов методов может привести к значительным потерям производительности и внести ошибки, связанные с потоками, в ваши родные модули. Кроме того, обратите внимание, что если вы решите использовать RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD
, ваше приложение больше не сможет использовать отладчик Google Chrome. Это связано с тем, что синхронные методы требуют, чтобы виртуальная машина JS делила память с приложением. Для отладчика Google Chrome React Native работает внутри JS VM в Google Chrome и асинхронно взаимодействует с мобильными устройствами через WebSockets.
Протестируйте то, что вы создали¶
На данном этапе вы создали базовый каркас для вашего нативного модуля в iOS. Проверьте это, обратившись к родному модулю и вызвав его экспортированный метод в JavaScript.
Найдите место в вашем приложении, где вы хотели бы добавить вызов метода createCalendarEvent()
родного модуля. Ниже приведен пример компонента NewModuleButton
, который вы можете добавить в свое приложение. Вы можете вызвать нативный модуль внутри функции NewModuleButton
onPress()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Чтобы получить доступ к нативному модулю из JavaScript, сначала нужно импортировать NativeModules
из React Native:
1 |
|
Затем вы можете получить доступ к нативному модулю CalendarModule
из NativeModules
.
1 |
|
Теперь, когда у вас есть нативный модуль CalendarModule, вы можете вызвать нативный метод createCalendarEvent()
. Ниже он добавлен в метод onPress()
в NewModuleButton
:
1 2 3 4 5 6 |
|
Последний шаг — пересобрать приложение React Native, чтобы у вас был доступен самый свежий нативный код (с вашим новым нативным модулем!). В командной строке, где находится приложение react native, выполните следующее :
1 |
|
Создание по мере итерации¶
По мере того, как вы будете работать по этим руководствам и итерировать свой нативный модуль, вам нужно будет перестроить свое приложение, чтобы получить доступ к последним изменениям из JavaScript. Это связано с тем, что код, который вы пишете, находится в нативной части вашего приложения. В то время как metro bundler React Native может следить за изменениями в JavaScript и перестраивать JS bundle на лету для вас, он не будет делать этого для нативного кода. Поэтому, если вы хотите протестировать последние изменения в нативном коде, вам нужно пересобрать его с помощью команды npx react-native run-ios
.
Recap✨¶
Теперь вы должны быть в состоянии вызвать метод createCalendarEvent()
вашего нативного модуля на JavaScript. Поскольку вы используете RCTLog
в функции, вы можете убедиться, что ваш родной метод вызывается, включив режим отладки в вашем приложении и посмотрев на консоль JS в Chrome или отладчике мобильных приложений Flipper. Вы должны увидеть сообщение RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
при каждом вызове метода нативного модуля.
На данном этапе вы создали нативный модуль iOS и вызвали его метод из JavaScript в вашем приложении React Native. Вы можете прочитать дальше, чтобы узнать больше о таких вещах, как типы аргументов, которые принимает метод вашего нативного модуля, и как настроить обратные вызовы и обещания в вашем нативном модуле.
За пределами нативного модуля календаря¶
Улучшенный экспорт нативного модуля¶
Импорт нативного модуля путем извлечения его из NativeModules
, как описано выше, немного неудобен.
Чтобы избавить потребителей вашего нативного модуля от необходимости делать это каждый раз, когда они хотят получить доступ к вашему нативному модулю, вы можете создать JavaScript-обертку для модуля. Создайте новый файл JavaScript с именем NativeCalendarModule.js
со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 |
|
Этот файл JavaScript также становится хорошим местом для добавления любой функциональности на стороне JavaScript. Например, если вы используете систему типов, такую как TypeScript, вы можете добавить сюда аннотации типов для вашего нативного модуля. Хотя React Native еще не поддерживает безопасность типов Native to JS, с этими аннотациями типов весь ваш JS-код будет безопасен для типов. Эти аннотации также облегчат вам переход на безопасные для типов нативные модули в будущем. Ниже приведен пример добавления безопасности типов в модуль Calendar
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
В других файлах JavaScript вы можете обратиться к родному модулю и вызвать его метод следующим образом:
1 2 |
|
!indo ""
1 |
|
Типы аргументов¶
Когда метод нативного модуля вызывается на JavaScript, React Native преобразует аргументы из JS-объектов в их объектные аналоги Objective-C/Swift. Так, например, если метод нативного модуля Objective-C принимает NSNumber, в JS вам нужно вызвать метод с числом. React Native выполнит преобразование за вас. Ниже приведен список типов аргументов, поддерживаемых методами нативного модуля, и их JavaScript-эквивалентов.
Objective-C | JavaScript |
---|---|
NSString | string, ?string |
BOOL | boolean |
NSNumber | ?boolean |
double | number |
NSNumber | ?number |
NSArray | Array, ?Array |
NSDictionary | Object, ?Object |
RCTResponseSenderBlock | Function (success) |
RCTResponseSenderBlock, RCTResponseErrorBlock | Function (failure) |
RCTPromiseResolveBlock, RCTPromiseRejectBlock | Promise |
Следующие типы поддерживаются в настоящее время, но не будут поддерживаться в TurboModules. Пожалуйста, избегайте их использования.
- Функция (отказ) -> RCTResponseErrorBlock
- Число -> NSInteger
- Число -> CGFloat
- Число -> float
Для iOS вы также можете писать собственные методы модуля с любым типом аргументов, который поддерживается классом RCTConvert
(смотрите RCTConvert для подробностей о том, что поддерживается). Все вспомогательные функции RCTConvert принимают на вход значение JSON и отображают его в собственный тип или класс Objective-C.
Экспорт констант¶
Родной модуль может экспортировать константы, переопределив родной метод constantsToExport()
. Ниже constantsToExport()
переопределяется и возвращает Dictionary, содержащий свойство имени события по умолчанию, к которому можно получить доступ в JavaScript следующим образом:
1 2 3 4 |
|
Затем к константе можно получить доступ, вызвав getConstants()
на родном модуле в JS следующим образом:
1 2 3 4 |
|
Технически, можно получить доступ к константам, экспортируемым в constantsToExport()
непосредственно из объекта NativeModule
. Это больше не будет поддерживаться в TurboModules, поэтому мы призываем сообщество перейти на описанный выше подход, чтобы избежать необходимой миграции в будущем.
Обратите внимание, что константы экспортируются только во время инициализации, поэтому если вы измените значения constantsToExport()
во время выполнения, это не повлияет на среду JavaScript.
Для iOS, если вы переопределите constantsToExport()
, вам также следует реализовать + requiresMainQueueSetup
, чтобы сообщить React Native, что ваш модуль должен быть инициализирован в главном потоке, до выполнения любого кода JavaScript. В противном случае вы увидите предупреждение, что в будущем ваш модуль может быть инициализирован в фоновом потоке, если вы явно не откажетесь от этого с помощью + requiresMainQueueSetup:
. Если вашему модулю не требуется доступ к UIKit, то на + requiresMainQueueSetup
следует ответить NO.
Обратные вызовы¶
Нативные модули также поддерживают уникальный вид аргумента — обратный вызов. Обратные вызовы используются для передачи данных из Objective-C в JavaScript для асинхронных методов. Они также могут быть использованы для асинхронного выполнения JS с нативной стороны.
Для iOS обратные вызовы реализованы с помощью типа RCTResponseSenderBlock
. Ниже параметр обратного вызова myCallBack
добавлен в createCalendarEventMethod()
:
1 2 3 |
|
Затем вы можете вызвать обратный вызов в вашей собственной функции, предоставив любой результат, который вы хотите передать JavaScript в массиве. Обратите внимание, что RCTResponseSenderBlock
принимает только один аргумент — массив параметров для передачи в обратный вызов JavaScript. Ниже вы передадите обратно ID события, созданного в предыдущем вызове.
Важно отметить, что обратный вызов не вызывается сразу после завершения родной функции — помните, что связь асинхронная.
1 2 3 4 5 6 7 |
|
Затем к этому методу можно получить доступ в JavaScript, используя следующее:
1 2 3 4 5 6 7 8 9 10 11 |
|
Родной модуль должен вызывать свой обратный вызов только один раз. Однако он может хранить обратный вызов и вызывать его позже. Этот паттерн часто используется для обертывания iOS API, требующих делегатов — см. пример в RCTAlertManager
. Если обратный вызов так и не будет вызван, произойдет утечка памяти.
Существует два подхода к обработке ошибок с помощью обратных вызовов. Первый — следовать соглашению Node и рассматривать первый аргумент, переданный в массив обратного вызова, как объект ошибки.
1 2 3 4 5 |
|
В JavaScript вы можете проверить первый аргумент, чтобы узнать, была ли передана ошибка:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Другой вариант — использовать два отдельных обратных вызова: onFailure и onSuccess.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Затем в JavaScript вы можете добавить отдельный обратный вызов для ответов об ошибке и успехе:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Если вы хотите передавать JavaScript объекты, похожие на ошибки, используйте RCTMakeError
из RCTUtils.h.
Сейчас это только передача JavaScript словаря в форме ошибки, но в будущем React Native планирует автоматически генерировать настоящие объекты JavaScript Error. Вы также можете предоставить аргумент RCTResponseErrorBlock
, который используется для обратных вызовов ошибок и принимает NSError \* object
. Обратите внимание, что этот тип аргумента не будет поддерживаться в TurboModules.
Промисы¶
Нативные модули также могут выполнять обещания, что может упростить ваш JavaScript, особенно при использовании синтаксиса async/await
в ES2016. Когда последним параметром метода нативного модуля является RCTPromiseResolveBlock
и RCTPromiseRejectBlock
, соответствующий JS-метод вернет объект JS Promise.
Рефакторинг приведенного выше кода для использования обещания вместо обратных вызовов выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
JavaScript-аналог этого метода возвращает Promise. Это означает, что вы можете использовать ключевое слово await
в асинхронной функции для вызова этого метода и ожидания его результата:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Отправка событий на JavaScript¶
Нативные модули могут сигнализировать JavaScript о событиях, не вызывая их напрямую. Например, вы можете захотеть передать на JavaScript напоминание о том, что скоро произойдет событие календаря из родного приложения календаря iOS. Предпочтительным способом сделать это является подкласс RCTEventEmitter
, реализовать supportedEvents
и вызвать self sendEventWithName
:
Обновите свой заголовочный класс, чтобы импортировать RCTEventEmitter
и подкласс RCTEventEmitter
:
1 2 3 4 5 6 7 |
|
Код JavaScript может подписаться на эти события, создав новый экземпляр NativeEventEmitter
вокруг вашего модуля.
Вы получите предупреждение, если будете тратить ресурсы без необходимости, испуская событие, пока нет слушателей. Чтобы избежать этого и оптимизировать нагрузку на ваш модуль (например, отписываясь от уведомлений, поступающих сверху, или приостанавливая фоновые задачи), вы можете переопределить startObserving
и stopObserving
в вашем подклассе RCTEventEmitter
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Threading¶
Если нативный модуль не предоставляет собственную очередь методов, он не должен делать никаких предположений о том, в каком потоке его вызывают. В настоящее время, если нативный модуль не предоставляет очередь методов, React Native создаст для него отдельную очередь GCD и вызовет его методы в ней. Обратите внимание, что это деталь реализации и может измениться. Если вы хотите явно предоставить очередь методов для нативного модуля, переопределите метод (dispatch_queue_t) methodQueue
в нативном модуле. Например, если модуль должен использовать API iOS только для основного потока, он должен указать это через:
1 2 3 4 |
|
Аналогично, если выполнение операции может занять много времени, нативный модуль может указать свою собственную очередь для выполнения операций. Опять же, в настоящее время React Native предоставляет отдельную очередь методов для вашего нативного модуля, но это деталь реализации, на которую не стоит полагаться. Если вы не предоставите собственную очередь методов, то в будущем долго выполняемые операции вашего нативного модуля могут заблокировать асинхронные вызовы, выполняемые в других несвязанных нативных модулях. Модуль RCTAsyncLocalStorage
здесь, например, создает свою собственную очередь, чтобы очередь React не блокировалась в ожидании потенциально медленного доступа к диску.
1 2 3 4 |
|
Указанная methodQueue
будет общей для всех методов вашего модуля. Если только один из ваших методов работает долго (или по какой-то причине должен выполняться в другой очереди, чем остальные), вы можете использовать dispatch_async
внутри метода, чтобы выполнить код этого конкретного метода в другой очереди, не затрагивая остальные:
1 2 3 4 5 6 7 8 9 |
|
Совместное использование очередей диспетчеризации между модулями
Метод methodQueue
будет вызван один раз при инициализации модуля, а затем сохранен React Native, поэтому нет необходимости хранить ссылку на очередь самостоятельно, если вы не хотите использовать ее внутри своего модуля. Однако если вы хотите использовать одну и ту же очередь в нескольких модулях, вам нужно убедиться, что вы сохраняете и возвращаете один и тот же экземпляр очереди для каждого из них.
Инъекция зависимостей¶
React Native автоматически создает и инициализирует все зарегистрированные нативные модули. Однако вы можете захотеть создать и инициализировать свои собственные экземпляры модулей, чтобы, например, внедрить зависимости.
Вы можете сделать это, создав класс, реализующий протокол RCTBridgeDelegate
, инициализируя RCTBridge
с делегатом в качестве аргумента и инициализируя RCTRootView
с инициализированным мостом.
1 2 3 4 5 6 7 8 |
|
Экспорт Swift¶
В Swift нет поддержки макросов, поэтому экспортирование нативных модулей и их методов в JavaScript внутри React Native требует немного больше настроек. Тем не менее, это работает относительно одинаково. Допустим, у вас есть тот же CalendarModule
, но в виде Swift-класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Важно использовать модификаторы @objc
, чтобы обеспечить правильный экспорт класса и функций в среду выполнения Objective-C.
Затем создайте частный файл реализации, который будет регистрировать необходимую информацию в React Native:
1 2 3 4 5 6 7 8 |
|
Для новичков в Swift и Objective-C, всякий раз, когда вы смешиваете два языка в проекте iOS, вам также понадобится дополнительный связующий файл, известный как связующий заголовок, чтобы открыть файлы Objective-C для Swift. Xcode предложит вам создать этот заголовочный файл, если вы добавите ваш Swift-файл в приложение с помощью опции меню Xcode File>New File
. Вам нужно будет импортировать RCTBridgeModule.h
в этот заголовочный файл.
1 2 |
|
Вы также можете использовать RCT_EXTERN_REMAP_MODULE
и _RCT_EXTERN_REMAP_METHOD
для изменения JavaScript-имени экспортируемого модуля или методов. Для получения дополнительной информации смотрите RCTBridgeModule
.
Важно при создании модулей сторонних разработчиков: Статические библиотеки с Swift поддерживаются только в Xcode 9 и более поздних версиях. Чтобы проект Xcode собрался при использовании Swift в статической библиотеке iOS, которую вы включаете в модуль, ваш основной проект приложения должен содержать код Swift и сам мостовой заголовок. Если ваш проект приложения не содержит кода Swift, обходным решением может быть один пустой файл .swift и пустой связующий заголовок.
Зарезервированные имена методов¶
invalidate()¶
Нативные модули могут соответствовать протоколу RCTInvalidating на iOS, реализуя метод invalidate()
. Этот метод может быть вызван, когда нативный мост становится недействительным (т.е. при перезагрузке devmode). Пожалуйста, используйте этот механизм по мере необходимости, чтобы выполнить требуемую очистку для вашего нативного модуля.