Всем привет, меня зовут Эдвард, и я 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.
2Grey
Так много вопросов к коду:
enum CodingKeys
у InitialCoords не нужен;Метод `init(from decoder) у InitialCoords тоже не нужен;
Зачем
var ymkMapView: YMKMapView!
, если можно простоvar ymkMapView: YMKMapView
(без восклицательного знака). И потом вinit
до вызоваsuper.init
присвоить значениеymkMapView
try!
плохая практика, что будет, если придет JSON, который не будет соответствовать структуре?mapView = MapView()
: каждый раз при вызове методаview()
будет создаваться новыйMapView
, можно просто объявитьlet mapView = MapView()
movpushmov Автор
Здравствуйте! Спасибо за замечания, учту! В свифте новичок)
V1tol
Ещё резанула взгляд конвертация в JSON и обратно. NSDictionary это и есть эквивалент жсного объекта, из него можно по ключам надёргать данные и положить в свою структуру. Тогда даже Decodable не понадобится.
movpushmov Автор
так-с, я скоро немножко исправлю код в статье, но есть некоторые нюансы:
1, 2) действительно, работает и без этого
3) Поменял методы init у MapView:
4) Исправил на вот такую конструкцию:
Если схема объекта неверная, то настройки просто не применятся.
5) Убрал переменную mapView из MapViewManager, но оставил создание MapView при каждом вызове view(), потому что иначе приложение будет крашится (в рн есть основной поток (UI) и побочный поток, в котором по дефолту делаются любые действия нативного модуля). Получилось как-то так:
Примеры кода для модуля взял тут