Привет! Меня зовут Георгий Мишин, я мобильный разработчик в СберМаркете. Хочу рассказать, как подключить Swift в React Native-проект. В рамках статьи создадим простенький нативный модуль и компонент, обсудим layout в React Native-приложении и поговорим, что же происходит в shadow thread. Это текстовая версия моего выступления на Android Meetup | СберМаркет Tech. Для того чтобы Swift заработал в React Native-проекте, нужно создать bridge-файл. XCode предложит сделать это автоматически при создании первого swift-файла. При разработке каждый экспортируемый в JavaScript элемент должен быть помечен атрибутом objc, чтобы его можно было использовать в Objective C runtime. Нативный модуль состоит из реализации на Swift и файла имплементации на Objective-C. На Swift пишем всю логику, а на Objective-C её имплементируем с помощью макросов из React Native, чтобы она подхватилась в JS. Простой пример нативного модуля Каждый идентификатор должен быть помечен атрибутом objc. Это класс, который будет экземпляром модуля и метод, который можно будет вызывать из JS. В файле имплементации указываем, какой модуль описываем, от какого класса наследуется и что он будет передавать. Можно обратить внимание на статический метод requiresMainQueueSetup. Он возвращает булевое значение, которое идентифицирует, где должен инициализироваться метод. Если возвращается «true», метод инициализируется до JavaScript, если «false» — во время или после. Документация говорит, что, переопределяя метод constantsToExport (для передачи каких-либо констант из React Native в JS), следует также определить requiresMainQueueSetup. Нужно вернуть «true», так как ваши определенные константы могут быть использованы в глобальной области, но разработчику виднее. Итак, есть нативный модуль с одним методом log. Чтобы использовать его на стороне React Native, импортируем из React Native объект NativeModules. В NativeModules, если все сделано правильно, будет объект с нашим модулем — название будет то, которое мы указали в RCTExternModule. Дальше можно обращаться ко всем сущностям этого модуля. Механизм передачи аргументов стандартный: у метода есть сигнатура, которая включает в себя различные входные и выходные параметры: Важно учесть, что в Swift и Objective-C в сигнатуре метода присутствует ярлык аргумента — префикс к входной переменной. Сейчас там пропуск, рассмотрим этот момент дальше отдельно. В методы можно передавать сериализуемые объекты и коллбэки. Доступные типы указаны в документации React Native. Можно передать как один параметр, так и несколько — так же, как с любым методом: Теперь на стороне JS можно передавать параметры как обычные аргументы функции. Все типы будут приведены автоматически. Возвращать из метода значение не рекомендуется. Поэтому основной способ взаимодействия JS и Native — коллбэки и промисы. Возвращать значения можно, такой метод будет называться синхронным, но документация не рекомендует использовать такой подход. Коллбэк — аргумент, который передаём так же, как и остальные аргументы. Его можем вызывать после какого-либо функционала. В этом случае коллбэк выполнится ровно после того, как информация попадёт на консоль. В качестве имплементации то же самое — просто указываем параметр, который будем использовать. Чтобы метод возвращал промис (Promise) и его можно было использовать с конструкцией async/await, в качестве двух последних аргументов метода нужно передать RCTPromiseResolveBlock и RCTPromiseRejectBlock. По сути, это те же самые коллбэки. Итак, мы разобрались, что: В качестве примера разберём простенький модуль выбора изображения из галереи. Это будет обычный PHImagePicker, который открывается из React Native. Он позволит выбрать одну фотографию, передать её по ссылке в файловой системе конкретному JS и отобразить в приложении. Сначала опишем, как это будет выглядеть на стороне JavaScript. Есть нативный модуль ImagePicker с одним методом — selectPhoto. Этот метод асинхронный и качестве resolve-параметра будет возвращать ссылку на фотографию, который мы сохраним в temp directory.  В UI будет рисоваться кнопка, которая откроет пикер и само изображение, если оно будет в стейте: Сам нативный модуль выглядит следующим образом: Нативный метод selectPhoto принимает resolve и reject. Тут можно обратить внимание на @escaping. Это означает, что передаваемые коллбэки могут «убежать» — вызваться не сразу в теле метода, а позже, либо не вызваться вообще. Подробнее можно прочитать в документации по Swift 5.7. Теперь мы сохраняем resolver и rejecter, чтобы вызвать в момент выбора фото. Получаем rootController — основной UIViewController, в котором «рисуется» всё React Native приложение. Создаём и настраиваем конфиг для PHPickerViewController и инициализируем его. А в конце устанавливаем delegate в pickerController (для этого модуль реализует протокол ФPHPickerViewControllerDelegate) и открываем его поверх основного. Реализованный протокол предоставляет нам метод func picker (PHPickerViewController, didFinishPicking: [PHPickerResult]), который вызовется с результатом, так как мы определили делегат. Далее в методе prepare мы обрабатываем этот результат и в случае успеха вызываем resolve (или reject в случае какой-либо ошибки). ||| Попробуем запустить. Если нажать на единственную кнопку «Выбрать фото», то получим ошибку. Она означает, что мы попытались вызвать функционал, который не должен вызываться в этом потоке. Нам нужно понимать, где исполняется наш метод. В данном случае мы используем компоненты UIKit (UIViewController), поэтому код должен исполняться в MainThread: Чтобы запустить функционал в MainThread, у React Native есть метод RCTExecuteOnMainQueue. В него нужно поместить блок кода, который нужно выполнить в MainThread: Тогда код будет работать отлично: откроется пикер, мы увидим фото в приложении. По нему можно кликать, и никакой проблемы нет. Для одного метода всё хорошо. Но если их много, зачем каждый раз писать RCTExecuteOnMainQueue? У React Native есть альтернатива — возможность установить поток исполнения для всего нативного модуля, определив метод methodQueue: Этот метод должен быть в инстансе нативного модуля и возвращать значение DispatchQueue. Теперь все наши нативные модули будут выполняться строго MainQueue. Теперь перейдём к реализации и архитектуре нативных компонентов. Сначала давайте посмотрим на архитектуру React Native-приложения:  Есть бридж, который гоняет JSON из JS в native и обратно. И Shadow thread, который строит весь layout — размеры и позиционирование относительно других элементов. Тут React Native о нас позаботился: не нужно отдельно устанавливать layout-параметры (высота, ширина, flex-свойства, paddings и так далее). Просто пишем компонент, описываем идеальные условия — и всё работает. Но такая схема вызывает некоторые сложности. Нативный компонент в первую очередь состоит из ViewManager — фабрики, которая при необходимости инициализирует UIView и ShadowView. По сути, ViewManager — тот же нативный модуль, но с другим базовым классом. Давайте напишем простой лейбл и посмотрим, как это всё будет работать и какие проблемы могут возникнуть при разработке своего нативного компонента. Сначала опишем класс SimpleTextViewManager и переопределим у него метод view, который должен возвращать UIView (из UIKit). Этот UIView и будет отображаться из RN. Далее создадим класс SimpleTextView, который будет наследоваться от UILabel (а он, в свою очередь, от UIView) и вернем его экземпляр из метода view() в SimpleViewManager. Добавим в SimpleTextView свойство content, которое будет устанавливать текст через React props. Такие параметры, как font, alignment, numberOfLines, захардкодим, но их тоже можно будет добавить. Чтобы свойство можно было контролировать через React props, как и с нативными модулями, каждое свойство в UIView нужно будет помечать атрибутом objc и зарегистрировать в файле имплементации UIManager. Переходим на сторону React Native: Чтобы подключить простой нативный компонент, у React Native существует функция requireNativeComponent. Она принимает в себя название компонента — в нашем случае это SimpleTextView. Получается вьюшка, у которой высота и ширина максимальные, а весь контент располагается по центру. И нативная вьюшка, которая растягивается по всему родительскому компоненту. Контент обычный, «Hello world». Давайте посмотрим, как это будет выглядеть на экране:  В инспекторе видим, что вьюшка занимает всё свободное место. Это логично: мы указали ей flex = 1. Но хотим ли мы, чтобы лейбл так себя вёл? Нет. А если уберём параметр flex, на экране просто ничего не будет, потому что Yoga посчитает высоту и ширину компонента, равную 0. Решить проблему, описанную выше, нам поможет ShadowView. Это класс, который содержит все необходимые свойства для Yoga engine и запускает функционал расчёта всего Layout. Задавать ему свойства можно через props компонента. Для решения задачи нам нужно создать класс, наследуемый от ShadowView, в который будем передавать параметры для расчёта layout. ShadowView — простой класс, у которого уже есть свойства для подсчёта layout: flex, width, height и другие. Чтобы высота и ширина нашего лейбла считались в зависимости от передаваемых туда параметров, нам пока нужно только свойство content. Наследуется он от класса RCTShadowView и принимает в себя только параметр content, по дефолту пустой. Теперь у ShadowView достаточно параметров, чтобы рассчитывать layout в зависимости от входящих props. Класс ShadowView содержит свойство yogaNode, которое и участвует в расчёте всего layout. Ему можно задавать различные свойства типа flex, flexShrink, flexBasis, height и другие. Но что делать, если высота и ширина контента неизвестна и должна зависеть от какого-либо параметра? Для этого в Yoga можно задать различные функции, например measureFunc. Она принимает YogaNode вместе с минимальной и максимальной допустимой высотой и шириной. А возвращает YGSize — итоговую высоту и ширину. В нашем случае ShadowView будет выглядеть следующим образом: Здесь мы: Теперь давайте посмотрим на нашу функцию measure. В ней мы: Давайте теперь запустим пример, убрав все style props в нашем компоненте, и посмотрим в layout-инспекторе, что получилось:  Высота получилась 51 поинт, а ширина — максимальная родительская. Можно добавить пэддинги у родителя, и при запуске они сработают. Вот и вся история — можно брать и применять на практике. Если остались вопросы или появились какие-то мысли, приходите в комментарии, пообщаемся. Мы завели соцсети с новостями и анонсами Tech-команды. Если хотите узнать, что под капотом высоконагруженного e-commerce, следите за нами там, где вам удобнее всего: Telegram, VK.