Настройте библиотеку Swift с помощью SwiftPM и Cocoapods.
Если вы работаете в крупной компании, вам может понадобиться работа с устаревшим кодом, написанным на Objective-C. В таких ситуациях может не потребоваться немедленный перенос кода на Swift. Вместо этого вы можете выполнить инкрементную миграцию, переписать конкретный код Swift и интегрировать его в кодовую базу Objective-C.
Например, предположим, что менеджер по продукту вашей компании просит пересмотреть процесс входа и добавить новую функцию. В этом случае вы можете переписать процесс входа в систему на Swift и интегрировать его в приложение Objective-C.
В этом руководстве будет показано, как создать библиотеку Swift, которая может работать в библиотеке Objective-C, и интегрировать ее в приложение Objective-C с помощью диспетчера пакетов Swift и Cocoapods.
Совместимость Swift и Objective-C
Apple позволила своим двум языкам, Swift и Objective-C, работать вместе. Однако не все возможности Swift можно использовать напрямую в Objective-C, а некоторые детали могут не освещаться в официальной документации.
Рассмотрим библиотеку Swift, которая предоставляет следующие объекты:
import Foundation public enum Operation { case add(first: Double, second: Double) case subtract(first: Double, second: Double) } public class Calculator { public init(){} public func compute(operation: Operation) -> Double { switch operation { case let .add(first, second): return first + second case let .subtract(first, second): return first - second } } }
Код определяет пару операций со связанными с ними значениями, используя enum
. Затем он предоставляет класс Calculator
, который может выполнять операции, определенные в классе enum
.
enum
со связанным значением нельзя использовать напрямую в Objective-C: Objective-C необходимо преобразовать перечисление в перечисление, которое можно сопоставить с целыми числами или строками.
Использование кода Swift в Objective-C возможно благодаря аннотации @objc
. Компилятор использует эту аннотацию для автоматического создания файлов заголовков, которые Objective-C использует для доступа к коду Swift.
Чтобы сохранить хорошее разделение задач, вы можете создать другой модуль, чтобы связать библиотеку Swift с возможным потребителем Objective-C. Код, необходимый для того, чтобы Calculator
работал с Objective-C, короткий, но содержит несколько концепций, поэтому давайте разделим его.
1. Предоставьте интероперабельное перечисление
Во-первых, вы должны создать перечисление для Objective-C, которое имитирует перечисление Operation
. Это необходимо, потому что Objective-C не может использовать enum
со связанными значениями.
Перечисление, совместимое с Objective-C, выглядит следующим образом:
@objc public enum ObjcOperation: Int { case add case subtract }
Перечисление:
- с аннотацией
@objc
- имеет необработанное значение
Int
Эти два условия удовлетворяют требованиям, предъявляемым к Objective-C для работы со Swift enum
s.
При использовании этого перечисления в Objective-C компилятор сгенерирует два значения, по одному для каждого случая: ObjcOperationAdd
и ObjcOperationSubtract
.
2. Сопоставьте связанные значения с объектами
Во-вторых, вам нужно сопоставить связанные значения enum
с реальными объектами, чтобы Objective-C мог их использовать.
Хотя в Objective-C можно использовать C-подобные структуры, прежний язык Apple не может использовать структуры Swift: все должно быть классом и должно расширять NSObject
.
Код для сопоставления связанных значений может выглядеть следующим образом:
@objc public class OperationData: NSObject { let first: Double let second: Double @objc public init(first: Double, second: Double) { self.first = first self.second = second } }
Обратите внимание, что OperationData
удовлетворяет всем требованиям для использования в мире Objective-C: он аннотирован @objc
, это класс и он расширяет NSObject
.
Он также определяет инициализатор public
, который также имеет аннотацию @objc
.
3. Преобразуйте операцию и OperationData в исходное перечисление.
Для третьего шага вам необходимо преобразовать новые экземпляры enum
и OperationData
в исходный код Swift. Есть несколько способов добиться этого: например, вы можете использовать шаблон Адаптер или расширить перечисление ObjcOperation
, чтобы возвращать правильную операцию при передаче OperationData
. Код может выглядеть так:
extension ObjcOperation { func toOperation(using data: OperationData) -> SwiftCode.Operation { switch self { case .add: return .add(first: data.first, second: data.second) case .subtract: return .subtract(first: data.first, second: data.second) } } }
Этот код не снабжен аннотацией @objc
; он используется внутри этого модуля и может использовать все мощные функции Swift.
Примечание. вы должны добавить префикс
SwiftCode.
к возвращаемому значению, чтобы явно указать Swift, какойOperation
он должен использовать. ФреймворкFoundation
уже предлагает классOperation
.
SwiftCode
— это имя модуля, в котором размещается чистое перечисление SwiftOperation
(подробнее об этом ниже). Это спойлер о хорошей практике организации кода: вы должны разделить исходную библиотеку и мост в два отдельных модуля. Тебе нужно
4. Создайте объект, имитирующий калькулятор
Последним шагом является имитация функций калькулятора в отдельном объекте. Код довольно прост, и он выглядит примерно так:
@objc public class InteropCalculator: NSObject { @objc public func compute( operation: ObjcOperation, operationData: OperationData ) -> Double { return Calculator() .compute(operation: operation.toOperation(using: operationData) } }
Код создает InteropCalculator
, который расширяет NSObject
и снабжается аннотацией @objc
, удовлетворяя всем требованиям для использования в Objective-C.
Он предлагает метод пересылки вычислений на исходный Swift Calculator
. Он использует расширение ObjcOperation
, определенное выше, для преобразования мостовых типов Objective-C в типы Swift.
Собрав все вместе, мостовой код выглядит так:
import Foundation import SwiftCode @objc public enum ObjcOperation: Int { case add case subtract } @objc public class OperationData: NSObject { let first: Double let second: Double @objc public init(first: Double, second: Double) { self.first = first self.second = second } } extension ObjcOperation { func toOperation(using data: OperationData) -> SwiftCode.Operation { switch self { case .add: return .add(first: data.first, second: data.second) case .subtract: return .subtract(first: data.first, second: data.second) } } } @objc public class InteropCalculator: NSObject { @objc public func compute( operation: ObjcOperation, operationData: OperationData ) -> Double { return Calculator() .compute( operation: operation.toOperation(using: operationData) ) } }
Обратите внимание, что вам нужно импортировать модуль SwiftCode
поверх файла.
Интеграция в приложение с помощью SwiftPM
Теперь, когда у вас есть библиотека и мост, вы можете интегрировать их в приложение Objective-C. Модульность кода — важный метод, позволяющий четко разделить код, выделить общедоступные интерфейсы и получить некоторые преимущества оптимизации, когда Xcode создает приложение постепенно.
SwiftPM — это система управления зависимостями, разработанная Apple, к которой движется все сообщество iOS. Это делает создание различных пакетов для обмена вашим кодом между библиотеками и приложениями чрезвычайно простым.
Простая установка этих модулей отражена в следующем файле Package.swift
:
// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "SwiftObjcInterop", products: [ .library( name: "SwiftObjcBridge", targets: ["SwiftObjcBridge"]), .library( name: "SwiftCode", targets: ["SwiftCode"]), ], targets: [ .target( name: "SwiftObjcBridge", dependencies: [.target(name: "SwiftCode")] ), .target(name: "SwiftCode"), ] )
Файлы пакета объявляют пакет с именем SwiftObjcInterop
, который экспортирует две разные библиотеки: SwiftObjcBridge
и SwiftCode
.
Он имеет две цели, которые имитируют структуру продуктов, и явно определяет, что SwiftObjcBridge
зависит от SwiftCode
.
Зачем определять две цели?
Одним из возможных возражений против текущего подхода является:
Зачем мне создавать две цели? Могу ли я просто реорганизовать цель
SwiftCode
с аннотацией и поддержкой Objective-C?
Ответ на второй вопрос: «Да, можете». Безусловно, можно иметь одну цель, которая поддерживает как Swift, так и Objective-C. Однако такое разделение кода имеет несколько преимуществ:
- Приложения Pure Swift могут импортировать напрямую
SwiftCode
и не заботятся о мосте. Эти пользователи ожидают какой-то идиоматический код Swift (например, они хотели бы использоватьenum
со связанными значениями), а не какую-то странную конструкцию, необходимую для Objective-C. - Это уменьшает количество необходимых зависимостей для чистых приложений Swift. Им не нужно импортировать две разные библиотеки. Чем меньше кода, тем лучше, и это создает меньше проблем.
- Он хорошо разделяет бизнес-логику и связующую логику, удовлетворяя принципу единой ответственности на уровне модулей.
- Будет проще отказаться от поддержки Objective-C. Никто больше не будет использовать Objective-C в какой-то момент в (далеком) будущем. Вы сможете удалить мост, а ядро библиотеки продолжит работать без изменений.
- Этот подход также работает для библиотек, которыми вы не владеете. Вы можете создать свой собственный мостовой модуль для сторонних чистых библиотек Swift, которые необходимо использовать с Objective-C.
Однако важно отметить и существенный недостаток: мостовой пакет жестко сцеплен с мостовым. Всякий раз, когда чистый пакет Swift изменяется, вам необходимо убедиться, что Bridge поддерживает все добавленные варианты использования и не ломается.
Интеграция в приложение
Теперь, когда вы определили свой пакет, вы готовы интегрировать его в свое приложение. Откройте приложение Objective-C и выполните следующие действия.
- Выберите проект в навигаторе проектов
- Выберите проект на верхней панели.
- Выберите Зависимости пакетов на панели вкладок.
- Выберите «Добавить локальный…» и выберите папку, содержащую пакет Swift.
Примечание. Не забудьте сначала закрыть проект Xcode с пакетом. В противном случае Xcode не сможет загрузить пакет в проект приложения.
Затем свяжите библиотеки с целью, чтобы сделать их доступными для приложения:
- Выберите цель приложения
- Прокрутите вниз до фреймворков, библиотек и встроенного контента.
- Нажмите кнопку +
- Добавьте обе библиотеки
SwiftCode
иSwiftObjcBridge
Окончательный результат должен выглядеть так:
Вызов пакета в приложении
Последним шагом является вызов кода в вашем приложении.
Например, откройте AppDelegate.m
своего приложения и добавьте следующий код:
#import "AppDelegate.h" // your other imports +@import SwiftObjcBridge; // ... other parts of the code ... -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // ... other initialization code +InteropCalculator *calc = InteropCalculator.new; +OperationData *data = [[OperationData alloc] initWithFirst:2 second:3]; +double result = [calc computeWithOperation:ObjcOperationAdd operationData:data]; +NSLog(@"Result is: %2.f", result); // ... rest of the app delegate }
Чтобы вызвать какой-либо мостовой код Swift, вам нужно всего лишь:
- используйте оператор
@import
, чтобы импортировать нужную библиотеку в базу кода - Используйте экспортированные объекты, как если бы они были собственным кодом Objective-C.
После запуска приложения консоль выдаст следующее:
Показывая, что интеграция работает правильно.
Интеграция в приложение с помощью Cocoapods
Если вы работаете с приложениями Objective-C, это, вероятно, означает, что приложение было реализовано задолго до SwiftPM. Скорее всего, ваша организация все еще использует другую систему управления зависимостями.
Cocoapods — важная альтернатива SwiftPM и широко используемая технология для обмена кодом между приложениями и библиотеками iOS. Чтобы настроить Cocoapods, выполните следующие действия:
- Установите Cocoapods на свой компьютер. Это можно сделать с помощью команды
$ sudo gem install cocoapods
, как указано в руководстве по установке. Тем не менее, я настоятельно рекомендую установить менеджер пакетов Ruby, напримерrbenv
, чтобы не связываться с установкой системы Ruby. - Определите
podspec
для ваших модулей (см. ниже) - Определите
podfile
для своего приложения (см. ниже) - Установите зависимости, выполнив команду
pod install
.
Определить Podspecs для модулей
Cocoapods работает с концепцией Pod, а не с пакетами, но они эквивалентны. Поды определяются в Ruby с помощью специальных файлов с именем podspec
s.
Podspec
s позволяет определить набор метаданных для зависимостей вместе с исходным кодом, который необходимо скомпилировать, и другими возможными зависимостями.
Начнем с определения podspec
для SwiftCode
:
Pod::Spec.new do |spec| spec.name = 'SwiftCode' spec.version = '1.0.0' spec.license = 'MIT' spec.homepage = 'https://github.com/cipolleschi/SwiftCode' spec.author = { 'Riccardo Cipolleschi' => '[email protected]' } spec.summary = "A Swift pod" spec.source = { :git => 'https://github.com/cipolleschi/SwiftCode.git', :tag => 'v1.0.0' } spec.source_files = "**/*.swift" spec.swift_version = '5.7' spec.ios.deployment_target = '14.0' end
Pod::Spec.new
инициализирует новый объект Spec
. Для этого требуется набор самоочевидных метаданных и некоторые конфигурации, которые стоит вызвать:
spec.source_files
определяет список всех необходимых исходных файлов.spec.swift_version
определяет минимальную поддерживаемую версию Swift.spec.ios.deployment_target
определяет минимальную поддерживаемую версию iOS.
Этот файл должен называться SwiftCode.podspec
, чтобы Cocoapods мог правильно прочитать его во время установки.
Затем вы должны определить podspec
для SwiftObjcBridge
. Код очень похож на предыдущий:
Pod::Spec.new do |spec| spec.name = 'SwiftObjcBridge' spec.version = '1.0.0' spec.license = 'MIT' spec.homepage = 'https://github.com/cipolleschi/SwiftObjcBridge' spec.author = { 'Riccardo Cipolleschi' => '[email protected]' } spec.summary = "A Swift pod" spec.source = { :git => 'https://github.com/cipolleschi/SwiftObjcBridge.git', :tag => 'v1.0.0' } spec.source_files = "**/*.swift" spec.swift_version = '5.7' spec.ios.deployment_target = '14.0' spec.dependency 'SwiftCode' end
Обязательно укажите, что этот модуль зависит от предыдущего, используя оператор spec.dependency 'SwiftCode'
.
podspec
должен называться SwiftObjcBridge.podspec
.
Определите подфайл приложения
Теперь вам нужно настроить приложение для правильной установки этих модулей. Это достигается с помощью другого файла Ruby с именем Podfile
.
Создайте его на том же уровне, что и файл .xcodeproj
. Его содержание примерно такое:
platform :ios, '14.0' target 'AnApp' do pod('SwiftCode', path: '../SwiftObjcInterop/Sources/SwiftCode') pod('SwiftObjcBridge', path: '../SwiftObjcInterop/Sources/SwiftObjcBridge') end
Podfile
указывает платформу и версию, которые будут использоваться для создания проекта.
Затем для каждой цели он позволяет определить список зависимостей. Зависимости можно настроить по-разному, например, используя версию или удаленный репозиторий. В этом случае вам все равно нужно опубликовать два модуля (пока), чтобы вы могли использовать локальные пути к ним. Параметр path
должен указывать на папку, содержащую файл podspec
для вашего модуля.
Установка зависимостей
После создания всех необходимых файлов вы можете установить зависимости в своем приложении. Откройте терминал и перейдите в папку, содержащую файл Podfile
. Затем выполните команду:
pod install
Консоль выведет что-то вроде этого, если все настроено правильно:
Cocoapods создает новый файл xcworkspace
, который станет вашим основным рабочим пространством для разработки. Затем вы можете open <yourApp>.xcworkspace
файл запустить Xcode и вызвать мостовой код в вашем приложении.
Вызов мостового кода в приложении
Изменения кода, необходимые для правильного вызова и выполнения кода Swift, присутствующего в этих модулях, такие же, как и в примере SwiftPM.
Это очень хорошо: вы можете легко поддерживать обе системы зависимостей, добавляя необходимые файлы конфигурации. Это не заставит ваших пользователей менять код при переходе с одной системы на другую!
Заключение
Все больше и больше компаний используют подход Swift-first, но во многих ситуациях вам нужно работать с Objective-C. Например, у вас может быть устаревшее приложение или вы можете разрабатывать библиотеку, которая должна предлагать поддержку Objective-C.
Сегодня вы узнали, как сопоставить чистый код Swift с эквивалентом, который может использовать Objective-C. В частности, вы сопоставили enum
со связанными значениями, но другие функции Swift требуют определенных сопоставлений.
Затем вы узнали, как настроить эту библиотеку с помощью SwiftPM и Cocoapods, разделив бизнес-логику и связующую логику на отдельные пакеты или модули. Этот подход имеет несколько преимуществ:
- более четкое разделение интересов
- меньше кода в чистых приложениях Swift
- меньше зависимостей для чистых приложений Swift
- простота удаления, если вам нужно будет удалить мост позже
- применимо к сторонним чистым библиотекам Swift, которым необходимо работать с Objective-C
Надеюсь, вам понравилось читать эту статью так же, как мне понравилось ее писать, и я надеюсь, что вы найдете ее полезной.