Розробка нативного модуля для React Native (iOS)
Ми вирішуємо задачу, яку не закрити готовим пакетом: Bluetooth Low Energy через CoreBluetooth, захищений Keychain через SecItemCopyMatching, інтеграція SDK банку або платіжної системи. Поки додаток працює тільки з JS-бібліотеками, все передбачувано. Але коли з'являється нестандартна потреба — доводиться писати нативний модуль вручну. І тут починається справжня інженерна робота, де наш 10-річний досвід у мобільній розробці дає гарантію стабільності та продуктивності. Зв'яжіться з нами для консультації — обговоримо ваш проект.
Типи даних через міст
Міст React Native приймає тільки типи, серіалізовані в JSON: NSString, NSNumber, NSArray, NSDictionary, NSNull. Бінарні дані (Data) кодуйте в Base64, кастомні об'єкти розбирайте в словник на нативній стороні. Це особливо критично при роботі з CoreBluetooth, де треба передавати CBCharacteristic з усіма властивостями. Неправильна серіалізація призводить до помилок часу виконання, які важко налагодити.
Проблеми, які вирішує нативний модуль
Bluetooth Low Energy. Стандартні пакети (react-native-ble-plx) не завжди підтримують кастомні протоколи або специфічні характеристики. Наш модуль обгортає CoreBluetooth з повним контролем над CBPeripheral, CBCentralManager та керуванням потоком даних.
Keychain і безпека. Зберігання токенів, ключів шифрування, біометричних даних потребує прямого доступу до SecItemCopyMatching. Помилки в імплементації призводять до витоків або крашів — ми використовуємо перевірений шаблон з потокобезпекою та коректною обробкою помилок.
Інтеграція SDK. Багато банків та платіжні системи надають тільки нативні бібліотеки (CocoaPods з Objective-C/Swift). Обгортка в нативний модуль — єдиний шлях до їх використання в React Native.
Як влаштований міст 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 — швидкість принципово інша. Порівняно з RCTBridgeModule, Turbo Module забезпечує продуктивність у 3-5 разів вище.
Проблема: якщо в проекті є хоча б один пакет без підтримки Turbo Module, New Architecture буде працювати в режимі сумісності, частково втрачаючи переваги. Наша команда допомагає провести аудит залежностей і спланувати міграцію без простою.
Як розробити нативний модуль: покроковий план
- Визначити вимоги до нативного 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 днів (від $300). Модуль із потоком подій, бінарними даними та підтримкою New Architecture — 5 днів і більше (від $500). Вартість розраховується індивідуально після аналізу вимог та кодової бази, але економія на адмініструванні сягає до 30%.
Отримайте консультацію — зв'яжіться з нами для попередньої оцінки вашого проекту. Замовте розробку нативного модуля у нас — гарантуємо стабільність та продуктивність.







