Всем привет, меня зовут Эдвард, и я 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 example
npx 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.