Привет! Меня зовут Игорь Сорокин, я занимаюсь iOS-разработкой в myTarget. Мы разрабатываем SDK для показа рекламы в мобильных приложениях. Недавно мы решили переписать его с Objective-C на Swift. Так как делать это мы решили итеративно, то какое-то время в нашем SDK должны уживаться два языка одновременно. Я расскажу, какие подходы используют для этого, почему нам не подошёл стандартный способ и что у нас из этого получилось. Статья будет полезна тем, кто разрабатывает SDK, используя оба языка, а также тем, кто хочет переехать с Objective-C на Swift.

P.S. Своим личным опытом, открытиями и интересными статьями я делюсь в Telegram-канале. Буду рад, если подпишитесь!
Что предлагает Apple?
В iOS стандартным способом совмещать код на Swift и Objective-C в SDK является использование Umbrella header и файла -Swift.h. Подробно эти способы описаны в документации (Swift → Objective-C, Objective-C → Swift). И поскольку эти файлы являются частью публичного интерфейса фреймворка, то объекты, содержащиеся в них, должны быть публичными. Это значит, что в Swift-коде мы сможем увидеть публичные (public) объекты Objective-C , а в коде на Objective-C — публичные Swift-объекты.
Проблемы возникают тогда, когда мы хотим получить доступ ко внутренним (internal) объектам. Из-за того, что мы переписываем SDK итеративно, нам было бы удобно, например, использовать внутренние Swift-объекты в публичных объектах Objective-C, и наоборот.
Поэтому мы решили найти способ импортировать внутренние объекты из одного языка в другой. Причём так как мы поставляем фреймворк в собранном виде, объекты не должны быть видны наружу.
Какие существуют варианты?
Swift → Objective-С
Сначала рассмотрим возможности импорта Swift в Objective-C. Нам удалось найти единственный рабочий вариант. Он основывается на маленькой хитрости. Дело в том, что объекты, помеченные @objc и унаследованные от NSObject, видны в среде исполнения Objective-C, их не видит только компилятор. Но мы можем ему помочь, создав header-файл (.h), который будет описывать объекты из Swift.
Фактически, при генерации файла -Swift.h происходит то же самое: компилятор ищет в Swift объекты, которые нужно экспортировать, а затем генерирует интерфейсы Objective-C для них и складывает в -Swift.h.
Написать такой header-файл можно несколькими способами:
-
вручную;
-
поменяв модификаторы доступа на public, собрать проект и сгенерировать -Swift.h, перенести из него нужные объявления в отдельный header-файл и вернуть модификаторы доступа на начальные.
Objective-C → Swift
Для импорта Objective-C в Swift мы нашли несколько адекватных вариантов.
-
Удалять .h-файлы из собранного фреймворка
Этот подход основывается на стандартном способе, то есть все объекты Objective-C нужно сделать публичными и добавить их в Umbrella header. Таким образом мы увидим их в Swift.
Хитрость заключается в том, что после сборки фреймворка нужно удалить внутренние .h-файлы из его Umbrella header. Так, во время разработки мы сможем использовать внутренние классы Objective-C, а вот разработчикам приложений они видны уже не будут. Подробнее, с примерами скрипта для удаления .h-файлов из Umbrella header, этот метод описан здесь.
Такой подход решает нашу задачу, однако всё равно остаётся риск «выпустить» внутренние объекты наружу.
-
Private module map
Второй способ использует private module map, который создаёт подмодуль в основном модуле. В private module map нужно указать внутренние .h-файлы, которые мы хотим видеть в Swift.
Module map — это специальный файл, который описывает модуль и .h-файлы в нём. Такой модуль может быть импортирован как в Swift, так и в Objective-C (с помощью
importили@import).Чтобы реализовать этот способ, нужно:
-
Описать подмодуль в файле modulemap:
// MyTargetSDK/module.modulemap explicit module MyTargetSDK.Internal { // List of your private headers. header "Private1.h" header "Private2.h" export * }-
В Build Settings фреймворка в поле
MODULEMAP_PRIVATE_FILEуказать путь до modulemap:
$(SRCROOT)/MyTargetSDK/module.modulemapТеперь можно импортировать в Swift наш подмодуль (
import MyTargetSDK.Internal), и из него будут доступны все файлы Objective-C, указанные в modulemap.Однако недостаток такого подхода заключается в том, что подмодуль на самом деле не является приватным, разработчики приложений смогут импортировать его себе и получить доступ ко всем внутренним объектам Objective-C. Поэтому этот способ нам не подошёл и мы выбрали следующий.
-
-
Module map
Как и в предыдущем способе, мы будем работать с module map, однако на этот раз создадим полностью отдельный модуль. Он будет доступен нашему SDK, но недоступен извне. Для этого нужно:
-
Создать modulemap и описать в нём приватный модуль:
// module.modulemap module MyTargetSDK_Internal { header "MyTargetSDK-InternalObjC.h" export * }MyTargetSDK-InternalObjC.h — это header-файл, содержащий внутренние .h-файлы, которые мы хотим использовать в Swift.
-
В Build Settings фреймворка в поле
SWIFT_INCLUDE_PATHSуказать путь до папки с module.modulemap.
Теперь можно импортировать
MyTargetSDK_Internalв Swift и использовать внутренние объекты Objective-C. Но если мы соберём такой фреймворк и подключим его в приложение, то получим ошибку:Could not find module 'MyTargetSDK_Internal' for target ...Всё дело в том, что если фреймворк А использует фреймворк Б, то при сборке А в его файлы .swiftinterface будет импортирован фреймворк Б. Для нас это значит, что модуль
MyTargetSDK_Internalимпортируется в MyTargetSDK. Но при подключении SDK в приложение компилятор не может найтиMyTargetSDK_Internal, потому что этот модуль только для разработки и его нет в собранном фреймворке.Чтобы такого не происходило, достаточно в SDK импортировать приватный модуль через модификатор @_implementationOnly:
@_implementationOnly import MyTargetSDK_InternalТем самым мы говорим компилятору, что импорт модуля является подробностью реализации, и поэтому наружу показывать его не нужно. Выполнение требований для
@_implementationOnlyобеспечивается компилятором. Например, если в публичном методе в качестве параметра будет использоваться объект изMyTargetSDK_Internal, то компилятор выдаст ошибку.Cannot use class 'InternalObjcClass' here; 'MyTargetSDK_Internal' has been imported as implementation-onlyТаким образом мы добились своей цели: внутренние объекты Objective-C можно использовать в Swift, причём они не будут видны извне и у нас нет возможности их случайно «выпустить».
-
Компромиссы
Конечно, некоробочные решения зачастую накладывают определённые трудности при разработке. В нашем подходе с отдельным внутренним модулем может возникнуть ситуация, когда один и тот же объект будет распознаваться компилятором как два разных. Это происходит из-за того, что объекты исходят из разных модулей. Легче объяснить это на примере. Представьте, что у нас есть Swift-класс и описывающий его .h-файл:
// LogInfo.swift @objc(MTRGLogInfo) final class LogInfo: NSObject { @objc var message: String = "" } // MTRGLogInfo.h @interface MTRGLogInfo : NSObject @property (nonatomic, copy) NSString *message; @end
В Objective-C у нас есть объект, который использует Swift-класс в качестве параметра:
// MTRGLoggerManager.h @class MTRGLogInfo; @interface MTRGLoggerManager: NSObject (void)logWithLogInfo:(MTRGLogInfo *)logInfo; @end
Этот MTRGLoggerManager мы хотим использовать в Swift, значит, добавляем его в наш module map:
// MyTargetSDK-InternalObjC.h #import "MTRGLoggerManager.h" #import "MTRGLogInfo.h"
В нём мы вынуждены помимо MTRGLoggerManager.h импортировать MTRGLogInfo.h, чтобы у нас был виден метод logWithLogInfo:. Теперь, если мы захотим использовать всё это в Swift, то получим следующую ошибку:
@_implementationOnly import MyTargetSDK_Internal final class SomeSwiftClass { func logViaLoggerManager() { let loggerManager = MTRGLoggerManager() let logInfo = LogInfo() // error: Cannot convert value of type 'LogInfo' to expected argument type 'MTRGLogInfo' loggerManager.log(with: logInfo) } }
Мы-то с вами знаем, что MTRGLogInfo и LogInfo— один и тот же объект, но для компилятора они разные, потому что исходят из разных модулей.

Боремся мы с этим просто: принудительно приводим один тип к другому.
@_implementationOnly import MyTargetSDK_Internal extension LogInfo { func objc() -> MTRGLogInfo { let logInfo = self as Any return logInfo as! MTRGLogInfo } } extension MTRGLogInfo { func swift() -> LogInfo { let logInfo = self as Any return logInfo as! LogInfo } }
При передаче объекта в функцию конвертируем его в нужный тип:
loggerManager.log(with: logInfo.objc())
Итог
Таким образом нам удалось добиться нашей цели, а именно — подружить языки между собой без необходимости делать объекты публичными. Мы выбрали оптимальный для себя подход, и пусть у него есть свои недостатки, тем не менее он решает нашу задачу.
Поделитесь в комментариях, сталкивались ли вы с подобными проблемами и как их решали?
ссылка на оригинал статьи https://habr.com/ru/articles/756470/
Добавить комментарий