Pull to refresh

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

Level of difficultyMedium
Reading time6 min
Views2.2K

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

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

Итак, со 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.

Tags:
Hubs:
Total votes 3: ↑2 and ↓1+1
Comments5

Articles