Написание нативных Swift модулей для React Native на примере Yandex Mapkit

от автора

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

Преднастройка проекта

  1. Устанавливаем зависимости (yarn / npm i / npm install)

  2. Добавляем зависимость в %название-вашей-либы%.podspec (4.3.1 — последняя версия на момент написания статьи)

    s.dependency "YandexMapsMobile", "4.3.1-full"

  3. cd example

  4. 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 struct InitialCoords: Decodable {   var lat: Double;   var lon: Double;      var zoom: Float;   var azimuth: Float;   var tilt: Float;      enum CodingKeys: String, CodingKey {     case lat     case lon     case zoom     case azimuth     case tilt   }      init(from decoder: Decoder) throws {     let container = try decoder.container(keyedBy: CodingKeys.self)     self.lat = try container.decode(Double.self, forKey: .lat)     self.lon = try container.decode(Double.self, forKey: .lon)     self.zoom = try container.decode(Float.self, forKey: .zoom)     self.azimuth = try container.decode(Float.self, forKey: .azimuth)     self.tilt = try container.decode(Float.self, forKey: .tilt)   } }  // функция-проверка, запускается код в симуляторе или на реальном устройстве 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 структуру         let json = try! JSONSerialization.data(withJSONObject: initialRegion, options: [])         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)     }          override init(frame: CGRect) {         super.init(frame: frame)                  // инициализация карты         if ymkMapView == nil {             // включаем свойство vulkanPreferred, если карта запускается в симуляторе,             // это нужно, чтобы не было лагов             ymkMapView = YMKMapView(frame: CGRect.zero, vulkanPreferred: isSimulator())             clipsToBounds = true             addSubview(ymkMapView)         }    }         required init?(coder aDecoder: NSCoder) {         super.init(coder: aDecoder)     } }  // создаём класс менеджера нашего View @objc(MapViewManager) class MapViewManager: RCTViewManager {     var mapView: MapView!      override static func requiresMainQueueSetup() -> Bool {         false     }        override func view() -> UIView! {         mapView = MapView()              return mapView     } }

Те люди, которые уже давно разрабатывают нативные модули на Objective C могут спросить, зачем я передаю в Swift NSDictionary, а не преобразовываю его в структуру с помощью RCTConvert внутри Objective C? Ответ простой:

коротко о причине, почему я потратил целых 2 дня впустую

коротко о причине, почему я потратил целых 2 дня впустую

Итак, со Swift-частью мы разобрались, теперь осталось написать Objective C экспорты и пойти проверять данное дело в симуляторе:

// 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 стороны:

// src/index.tsx import {   Platform,   requireNativeComponent,   UIManager,   ViewStyle, } from 'react-native';  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';  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';  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 view, которые передаётся в нативный компонент, а это значит, что мы будем делать маркеры для нашей нативной карты)

Деятельность экстремистской организации (признана такой 21 марта 2022 года) Meta Platforms или Meta* запрещена в России. Компания владеет социальными сетями Facebook** и Instagram.


ссылка на оригинал статьи https://habr.com/ru/articles/736106/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *