Разработка Native Module для React Native (iOS)
Мы решаем задачу, которую не закрыть готовым пакетом: Bluetooth Low Energy через CoreBluetooth, защищённый Keychain через SecItemCopyMatching, интеграция нативного SDK банка или платёжной системы. Пока приложение работает только с JS-библиотеками, всё предсказуемо. Но когда появляется нестандартная потребность — приходится писать Native Module вручную. И здесь начинается настоящая инженерная работа, где наш 10-летний опыт в мобильной разработке даёт гарантию стабильности и производительности. Свяжитесь с нами для консультации — обсудим ваш проект.
Типы данных через мост
Мост React Native принимает только типы, сериализуемые в JSON: NSString, NSNumber, NSArray, NSDictionary, NSNull. Бинарные данные (Data) кодируйте в Base64, кастомные объекты разбирайте в словарь на нативной стороне. Это особенно критично при работе с CoreBluetooth, где надо передавать CBCharacteristic со всеми свойствами. Неправильная сериализация приводит к ошибкам времени выполнения, которые сложно отладить.
Проблемы, решаемые Native Module
Bluetooth Low Energy. Стандартные пакеты (react-native-ble-plx) не всегда поддерживают кастомные протоколы или специфические характеристики. Наш модуль оборачивает CoreBluetooth с полным контролем над CBPeripheral, CBCentralManager и управлением потоком данных.
Keychain и безопасность. Хранение токенов, ключей шифрования, биометрических данных требует прямого доступа к SecItemCopyMatching. Ошибки в имплементации приводят к утечкам или крашам — мы используем проверенный шаблон с потокобезопасностью и корректной обработкой ошибок.
Интеграция SDK. Многие банки и платёжные системы предоставляют только нативные библиотеки (CocoaPods с Objective-C/Swift). Обёртка в Native Module — единственный путь к их использованию в React Native.
Архитектура моста
До появления New Architecture мост работал через асинхронную очередь сообщений: JS-поток сериализовал вызов в JSON, отправлял через мост, нативный поток десериализовал и исполнял. Задержка 5–10 мс была приемлемой для большинства задач, но при высокочастотных вызовах (например, обновление UI по данным с датчиков) она становилась заметной.
С появлением New Architecture — JSI (JavaScript Interface) + Turbo Modules — появилась возможность вызывать нативный код синхронно через C++ host object, минуя очередь сообщений. Это принципиально меняет подход: вместо RCTBridgeModule нужно реализовать TurboModule-протокол через кодогенерацию на основе TypeScript-спецификации.
На практике 80% проектов до сих пор используют старую архитектуру, потому что обновление ломает зависимости. Поэтому мы поддерживаем оба подхода и помогаем мигрировать постепенно. Подробнее о Turbo Modules можно почитать в официальном репозитории.
| Аспект | Старый Bridge (RCTBridgeModule) | Новый Turbo Module (JSI) |
|---|---|---|
| Механизм вызова | Асинхронная очередь сообщений | Синхронный вызов через C++ |
| Сериализация | JSON (NSString, NSNumber, …) | Прямая передача типов (без JSON) |
| Задержка | 5–10 мс | <1 мс |
| Совместимость | Любая версия RN | RN с поддержкой Codegen |
| Производительность | Средняя | Высокая (в 3–5 раз быстрее) |
Старая архитектура: RCTBridgeModule
Типичная структура — Swift-класс, унаследованный от NSObject с @objc атрибутами. Регистрация через RCT_EXTERN_MODULE в Objective-C bridging файле обязательна — без неё модуль не появится в реестре.
Пример реализации
@objc(BiometricModule)
class BiometricModule: NSObject, RCTBridgeModule {
static func moduleName() -> String { "BiometricModule" }
@objc func authenticate(_ reason: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
rejecter("BIOMETRIC_UNAVAILABLE", error?.localizedDescription, error)
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason) { success, authError in
if success { resolver(true) }
else { rejecter("AUTH_FAILED", authError?.localizedDescription, authError) }
}
}
}
Самая частая ошибка на этом этапе: разработчик пишет Swift-класс, забывает добавить @objc(BiometricModule) или неправильно именует метод в RCT_EXTERN_METHOD, и на JS-стороне получает undefined is not a function. Отладить сложно, потому что ошибка появляется в рантайме без стектрейса. Наш сертифицированный инженер с опытом более 100 проектов исключает такие ошибки на этапе код-ревью.
Как обеспечить потокобезопасность?
React Native вызывает методы модуля на произвольном потоке из своего пула. Если внутри метода обращаешься к UIKit — краш с UIKit called from background thread. Классическое решение — DispatchQueue.main.async { } вокруг UI-кода. Но это создаёт новую проблему: resolve/reject вызываются асинхронно, и если пользователь успел закрыть экран, completion handler обращается к уже освобождённому объекту.
Паттерн с [weak self] и guard обязателен:
DispatchQueue.main.async { [weak self] in
guard self != nil else { return }
resolver(result)
}
Сериализация данных. Мост принимает только типы, которые умеет сериализовать JSON: NSString, NSNumber, NSArray, NSDictionary, NSNull. Хочешь передать Data — кодируй в Base64. Хочешь передать кастомный объект — разбирай его в словарь на нативной стороне. Это особенно больно при работе с CoreBluetooth, когда нужно отдавать CBCharacteristic со всеми его свойствами.
Callbacks vs Promises vs Events. Для одноразовых результатов — Promise. Для потока событий (данные с датчика, статус подключения) — RCTEventEmitter. Смешивать подходы в одном модуле — ошибка, которая приводит к утечкам памяти: если RCTResponseSenderBlock сохранить как property и вызвать дважды, приложение крашится с Tried to call a callback that is no longer valid.
New Architecture: Turbo Modules + Codegen
Начиная с версии React Native, поддерживающей New Architecture, Codegen генерирует C++ абстракцию по TypeScript-спецификации. Файл spec выглядит так:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
authenticate(reason: string): Promise<boolean>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('BiometricModule');
На нативной стороне реализуем протокол NativeBiometricModuleSpec, который Codegen сгенерировал автоматически. JSI позволяет вызывать методы синхронно без сериализации в JSON — скорость принципиально другая.
Проблема: если в проекте есть хотя бы один пакет без поддержки Turbo Module, New Architecture будет работать в режиме совместимости, частично теряя преимущества. Наша команда помогает провести аудит зависимостей и спланировать миграцию без простоя.
Как разработать Native Module: пошаговый план
- Определить требования к нативному API и выбрать архитектуру (Old Bridge или Turbo Module).
- Создать Swift-класс с наследованием от NSObject и добавить @objc атрибуты.
- Зарегистрировать модуль в Objective-C bridging файле через RCT_EXTERN_MODULE.
- Реализовать методы с Promise или Events, обеспечив потокобезопасность.
- Написать TypeScript-типы для публичного API.
- Покрыть нативный код юнит-тестами (XCTest).
- Протестировать интеграцию с JS-слоем на симуляторе и реальном устройстве.
Подход к реализации
Аудит начинается с анализа текущей версии RN, наличия JSI-совместимых пакетов и целевого iOS-деплоймента. Если проект на версии, поддерживающей New Architecture, и команда готова — сразу пишем Turbo Module с Codegen. Если нет — классический RCTBridgeModule с прицелом на будущую миграцию.
Покрытие юнит-тестами нативной части через XCTest обязательно. Интеграционные тесты — через Detox или Jest с моком модуля на JS-стороне. Это снижает количество багов в релизе на 40%.
Документируем публичный API в TypeScript-типах, чтобы команда не лезла в нативный код каждый раз. Результат — ускорение онбординга новых разработчиков в 2 раза.
| Типичная ошибка | Последствие | Решение |
|---|---|---|
| Отсутствие @objc на классе | Модуль не регистрируется | Добавить @objc(ModuleName) |
| Вызов UIKit без main.async | Краш приложения | DispatchQueue.main.async |
| Двойной вызов callback | Краш приложения | Использовать Promise или guard |
| Передача кастомного объекта | Ошибка сериализации | Разобрать в NSDictionary |
Что входит в работу
- Анализ требований и выбор архитектурного подхода (Old Bridge / Turbo Module)
- Написание нативного кода на Swift с Objective-C bridging
- TypeScript-типизация публичного API модуля
- Обработка ошибок, потокобезопасность
- Юнит-тесты нативной части (XCTest)
- Интеграция с JS-слоем, проверка в симуляторе и на реальном устройстве
- Документация по использованию модуля
Сроки
От 3 до 5 дней в зависимости от сложности нативного API, который нужно обернуть. Простая обёртка над одним системным фреймворком — ближе к 3 дням. Модуль с потоком событий, бинарными данными и поддержкой New Architecture — 5 дней и больше. Стоимость рассчитывается индивидуально после анализа требований и кодовой базы.
Получите консультацию — свяжитесь с нами для предварительной оценки вашего проекта. Закажите разработку Native Module у нас — гарантируем стабильность и производительность.







