Всем привет, меня зовут Эдвард, и я Middle Front-end разработчик в команде Stellar 2H Group. Недавно я начал изучение разработки нативных view / модулей под React Native и хотел бы поделиться этим опытом, потому что мне пришлось столкнуться с некоторыми трудностями, о которых я позже поведаю.
В данной статье я буду использовать Webstorm и XCode. Если статья найдёт свой отклик, то попробуем реализовать то же самое, но под android. Приятного чтения!
Небольшой экскурс для тех, кто не в теме
React Native — это кроссплатформенный фреймворк с открытым исходным кодом для разработки нативных мобильных и настольных приложений на JavaScript и TypeScript, созданный Facebook Inc. (ныне Meta*)
Нативные модули для этой технологии пишутся на нативных языках хост-платформ (ios/android/windows/mac os). Например Objective C, Swift, Kotlin, C++.
В принципе, этой информации должно быть достаточно для минимального понимания, что здесь вообще происходит.
А что насчёт архитектуры?
На данный момент в RN реализовали новую архитектуру, которая называется Fabric, но её мы затрагивать не будем, поскольку в официальной документации сказано, что она экспериментальная и находится в активной разработке. источник
Создаём проект
Здесь всё просто. Запускаем вот эту команду, выбираем пункт native view, далее Kotlin & Swift и ждём, пока создастся темплейт проекта:
npx create-react-native-library@latest react-native-awesome-mapkit
Преднастройка проекта
Устанавливаем зависимости (
yarn/npm i/npm install)Добавляем зависимость в %название-вашей-либы%.podspec (4.3.1 - последняя версия на момент написания статьи)
s.dependency "YandexMapsMobile", "4.3.1-full"cd examplenpx pod-install
Готово! Мы можем писать нашу библиотеку
Шаг 0: Открываем проект
Открываем example/ios/AwesomeMapkitExample.xcworkspace в XCode
Шаг 1: Устанавливаем ключ Yandex Mapkit и язык карты
В example/ios/AwesomeMapkitExample/AppDelegate.mm прописываем следующие строчки:
#import <YandexMapsMobile/YMKMapKitFactory.h> ... - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"AwesomeMapkitExample"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; [YMKMapKit setApiKey:@"Ваш API ключ"]; // необязательное действие. По дефолту язык системы [YMKMapKit setLocale:@"ru_RU"]; [[YMKMapKit mapKit] onStart]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }
Отлично. Теперь при запуске приложения у нас будет проставляться API ключ Yandex карт. Замечу, что хардкод ключа и языка карты это временная мера. В следующих статьях мы сделаем возможность проставлять этот ключ и без доступа в натив (тот же самый expo-dev-client)
Шаг 2: Создаём нативный view карт
в корне находим папку ios и создаём папку MapView, а затем два файлика внутри: MapView.m и MapView.swift target выставляем Pods-AwesomeMapkitExample
При создании .swift файла XCode предложит создать bridging header. Не делаем этого, он уже есть, так как мы выбрали тип проекта Swift + Kotlin
Сначала напишем Swift часть нашего MapView:
import Foundation import React import YandexMapsMobile /* объявляем структуру InitialCoords, которая реализует протокол Decodable протокол Decodable поможет нам преобразовать тип NSDictionary (наш JS объект) в Swift-структуру. */ struct InitialCoords: Decodable { var lat: Double; var lon: Double; var zoom: Float; var azimuth: Float; var tilt: Float; } // функция-проверка, запускается код в симуляторе или на реальном устройстве func isSimulator() -> Bool { #if targetEnvironment(simulator) return true #else return false #endif } // класс нативного view, который потом будет отдан в отрисовку @objcMembers class MapView: UIView { // нативный View Yandex карты var ymkMapView: YMKMapView // функция, которая принимает JS объект, передаваемый в пропс initialRegion func setInitialRegion(_ initialRegion: NSDictionary) { /* Декодируем объект в swift структуру. Если в одном из if-ов try словит ошибку, то вернётся пустой объект и if не отработает, следовательно, настройки при неверной схеме объекта не применятся */ if let json = try? JSONSerialization.data(withJSONObject: initialRegion, options: []) { if let region: InitialCoords = try? JSONDecoder().decode(InitialCoords.self, from: json) { // создаём точку, которая будет являться центром камеры let cameraPoint = YMKPoint(latitude: region.lat, longitude: region.lon) // создаём камеру let cameraPosition = YMKCameraPosition(target: cameraPoint, zoom: region.zoom, azimuth: region.azimuth, tilt: region.tilt) // передвигаем обзор карты на нужную точку без анимации ymkMapView.mapWindow.map.move(with: cameraPosition, animationType: YMKAnimation(type: YMKAnimationType.linear, duration: 0), cameraCallback: nil) } } } /* Инициализация карты яндекса. Указываем дефолтный нулевой фрейм для создания объекта, потом добавляем в subview и делаем clipsToBounds = true, чтобы карта растягивалась на весь родительский view */ override init(frame: CGRect) { ymkMapView = YMKMapView(frame: CGRect.zero, vulkanPreferred: isSimulator()) super.init(frame: frame) clipsToBounds = true addSubview(ymkMapView) } required init?(coder aDecoder: NSCoder) { ymkMapView = YMKMapView(frame: CGRect.zero, vulkanPreferred: isSimulator()) super.init(coder: aDecoder) clipsToBounds = true addSubview(ymkMapView) } } /* Создаём класс менеджера нашего View. Менеджер - это класс, который производит первую настройку нативного компонента (requiresMainQueueSetup) и отдаёт нативный view для отрисовки. Затем этот класс будет использован для прокидывания в РН с помощью макросов в Objective-C (RCT_EXTERN_MODULE) */ @objc(MapViewManager) class MapViewManager: RCTViewManager { /* Этот метод вызывается только при инициализации, если ваш метод инициализации вызывает пользовательский интерфейс или вы переопределяете сonstantToExport, то ставим true */ override static func requiresMainQueueSetup() -> Bool { true } override func view() -> UIView! { return MapView() } }
Те люди, которые уже давно разрабатывают нативные модули на Objective C могут спросить, зачем я передаю в Swift NSDictionary, а не преобразовываю его в структуру с помощью RCTConvert внутри Objective C? Ответ простой:

Итак, со Swift-частью мы разобрались, теперь осталось написать Objective C экспорты, подправить JS сторону и пойти проверять данное дело в симуляторе:
// MapView.m #import <Foundation/Foundation.h> #import <React/RCTViewManager.h> #import <React/RCTBridgeModule.h> // экспортируем наш MapViewManager, реализация которого находится в MapView.swift @interface RCT_EXTERN_MODULE(MapViewManager, RCTViewManager) // экспортируем пропс initialRegion RCT_EXPORT_VIEW_PROPERTY(initialRegion, NSDictionary) @end
Пишем JS-сторону нашего модуля, экспортируем нативный View
// src/index.tsx import { Platform, requireNativeComponent, UIManager, ViewStyle, } from 'react-native'; // Пропсы нативного View type MapViewProps = { style?: ViewStyle; initialRegion: { lat: number; lon: number; zoom: number; azimuth: number; tilt: number; }; }; const LINKING_ERROR = `The package 'react-native-awesome-mapkit' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; const ComponentName = 'MapView'; /* Получаем config нативного view, если он пустой, то выдаём ошибку. Это значит, что наш view не экспортнулся по какой-то причине Если же конфиг не пустой, значит импортируем нативный view и отдаём его */ export const MapView = UIManager.getViewManagerConfig(ComponentName) != null ? requireNativeComponent<MapViewProps>(ComponentName) : () => { throw new Error(LINKING_ERROR); };
// example/src/App.tsx import * as React from 'react'; import { MapView } from 'react-native-awesome-mapkit'; // Добавляем нативный view и передаём пропсы, которые мы у��азали в нативном модуле export default function App() { return ( <MapView style={{ flex: 1 }} initialRegion={{ lat: 55.751574, lon: 37.573856, zoom: 15, azimuth: 0, tilt: 0, }} /> ); }
Шаг 3: Радуемся жизни
Запускаем проект так:
yarn example start
yarn example ios
Если вы увидели примерно такую картину, то поздравляю, вы всё сделали правильно! Ура!

В следующей части я покажу, как контролировать children views, которые передаётся в нативный компонент, а это значит, что мы будем делать маркеры для нашей нативной карты)
Деятельность экстремистской организации (признана такой 21 марта 2022 года) Meta Platforms или Meta* запрещена в России. Компания владеет социальными сетями Facebook** и Instagram.