Настройте библиотеку 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 enums.

При использовании этого перечисления в 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 — это имя модуля, в котором размещается чистое перечисление Swift Operation (подробнее об этом ниже). Это спойлер о хорошей практике организации кода: вы должны разделить исходную библиотеку и мост в два отдельных модуля. Тебе нужно

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. Однако такое разделение кода имеет несколько преимуществ:

  1. Приложения Pure Swift могут импортировать напрямую SwiftCode и не заботятся о мосте. Эти пользователи ожидают какой-то идиоматический код Swift (например, они хотели бы использовать enum со связанными значениями), а не какую-то странную конструкцию, необходимую для Objective-C.
  2. Это уменьшает количество необходимых зависимостей для чистых приложений Swift. Им не нужно импортировать две разные библиотеки. Чем меньше кода, тем лучше, и это создает меньше проблем.
  3. Он хорошо разделяет бизнес-логику и связующую логику, удовлетворяя принципу единой ответственности на уровне модулей.
  4. Будет проще отказаться от поддержки Objective-C. Никто больше не будет использовать Objective-C в какой-то момент в (далеком) будущем. Вы сможете удалить мост, а ядро ​​библиотеки продолжит работать без изменений.
  5. Этот подход также работает для библиотек, которыми вы не владеете. Вы можете создать свой собственный мостовой модуль для сторонних чистых библиотек Swift, которые необходимо использовать с Objective-C.

Однако важно отметить и существенный недостаток: мостовой пакет жестко сцеплен с мостовым. Всякий раз, когда чистый пакет Swift изменяется, вам необходимо убедиться, что Bridge поддерживает все добавленные варианты использования и не ломается.

Интеграция в приложение

Теперь, когда вы определили свой пакет, вы готовы интегрировать его в свое приложение. Откройте приложение Objective-C и выполните следующие действия.

  1. Выберите проект в навигаторе проектов
  2. Выберите проект на верхней панели.
  3. Выберите Зависимости пакетов на панели вкладок.
  4. Выберите «Добавить локальный…» и выберите папку, содержащую пакет Swift.

Примечание. Не забудьте сначала закрыть проект Xcode с пакетом. В противном случае Xcode не сможет загрузить пакет в проект приложения.

Затем свяжите библиотеки с целью, чтобы сделать их доступными для приложения:

  1. Выберите цель приложения
  2. Прокрутите вниз до фреймворков, библиотек и встроенного контента.
  3. Нажмите кнопку +
  4. Добавьте обе библиотеки 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, вам нужно всего лишь:

  1. используйте оператор @import, чтобы импортировать нужную библиотеку в базу кода
  2. Используйте экспортированные объекты, как если бы они были собственным кодом Objective-C.

После запуска приложения консоль выдаст следующее:

Показывая, что интеграция работает правильно.

Интеграция в приложение с помощью Cocoapods

Если вы работаете с приложениями Objective-C, это, вероятно, означает, что приложение было реализовано задолго до SwiftPM. Скорее всего, ваша организация все еще использует другую систему управления зависимостями.

Cocoapods — важная альтернатива SwiftPM и широко используемая технология для обмена кодом между приложениями и библиотеками iOS. Чтобы настроить Cocoapods, выполните следующие действия:

  1. Установите Cocoapods на свой компьютер. Это можно сделать с помощью команды $ sudo gem install cocoapods, как указано в руководстве по установке. Тем не менее, я настоятельно рекомендую установить менеджер пакетов Ruby, например rbenv, чтобы не связываться с установкой системы Ruby.
  2. Определите podspec для ваших модулей (см. ниже)
  3. Определите podfile для своего приложения (см. ниже)
  4. Установите зависимости, выполнив команду pod install.

Определить Podspecs для модулей

Cocoapods работает с концепцией Pod, а не с пакетами, но они эквивалентны. Поды определяются в Ruby с помощью специальных файлов с именем podspecs.

Podspecs позволяет определить набор метаданных для зависимостей вместе с исходным кодом, который необходимо скомпилировать, и другими возможными зависимостями.

Начнем с определения 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

Надеюсь, вам понравилось читать эту статью так же, как мне понравилось ее писать, и я надеюсь, что вы найдете ее полезной.