Несмотря на то, что Apple предоставила нам подробную документацию о том, как использовать код Swift внутри приложения Objective-C (и наоборот), этого недостаточно. Когда мне понадобилось предоставить Swift framework с совместимостью с Objective-C, документация Apple вызвала больше вопросов, чем дала ответы (или, по крайней мере, оставила много пробелов). Интенсивный поиск показал, что тема освещена очень плохо: пара вопросов по StackOverflow и несколько вводных статей - вот и все, что я нашел.
Этот пост является обобщением найденной информации и моего собственного опыта. Все описанные методы не претендуют на звание действительно хорошей практики, они просто предлагают способ решения проблемы.
TL; DR. Чтобы использовать код Swift внутри Objective-C, нужно урезать некоторые функции Swift и написать оболочку вокруг исходного кода Swift, который не будет использовать несовместимые функции (например, структуры, Дженерики, перечисление связанных значений, расширения протокола и т. Д.). Все классы-оболочки должны наследовать
NSObject
.
Начало
Итак, у нас есть проект на основе Objective-C и необходимый модуль кода Swift. Это может быть фреймворк Swift, добавленный с помощью CocoaPods. В этом случае, как обычно, добавляем зависимость в Podfile, запускаем pod install
, открываем xcworkspace
файл.
Чтобы сделать фреймворк Swift видимым, нет необходимости использовать import
ни весь модуль (как мы это делали в Swift), ни отдельные файлы (как мы привыкли делать в Objective-C). Что необходимо импортировать, так это файл с именем <ProjectName>-Swift.h
- это автоматически сгенерированный файл заголовка, который должен быть связующим звеном между кодом Objective-C и общедоступными API Swift. Это выглядит так:
#import "YourProjectName-Swift.h"
Классы
Если вы можете использовать какой-либо класс или метод Swift в Objective-C из коробки, вам повезло: кто-то позаботился о совместимости за вас. Дело в том, что Objective-C переваривает только публичные API NSObject
наследников, а свойства, инициализаторы и методы должны быть помечены @objc
атрибутом.
Когда вы имеете дело со своим собственным кодом, вы всегда можете унаследовать все, что захотите, и добавить любой атрибут. В этом случае, конечно, вы даже можете писать на Objective-C, не так ли? Таким образом, лучше сосредоточиться на чужом коде. Что мы можем сделать? Писать обертки.
Например, рассмотрим этот класс Swift:
public class SwiftClass { public func swiftMethod() { // Implementation goes here. } }
Мы создаем наш собственный файл Swift, импортируем внешний модуль Swift, создаем NSObject
дочерний класс и частное свойство желаемого типа Swift внутри класса. Главное - это метод, который проксирует исходный метод Swift над свойством:
import Foundation import SwiftFramework public class SwiftClassObjCWrapper: NSObject { private let swiftClass = SwiftClass() @objc public func swiftMethod() { swiftClass.swiftMethod() } }
(Атрибуты NSObject
и @objc
доступны в Foundation.)
Очевидно, мы не можем использовать одни и те же имена. Но мы можем предоставить Objective-C доступ к API-оболочкам, используя оригинальные имена:
@objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { // ...
Теперь вызовы Objective-C выглядят как оригинальные вызовы классов Swift:
SwiftClass *swiftClass = [SwiftClass new]; [swiftClass swiftMethod];
Методы
К сожалению, мы не можем пометить @objc
какой-либо общедоступный метод и просто использовать его в Objective-C. Мы имеем дело с разными языками программирования, которые часто имеют разные возможности, а иногда реализуем похожие вещи, используя разную логику. Некоторые функции Swift просто отсутствуют в Objective-C.
Например, мы должны забыть о значениях аргументов по умолчанию. Этот способ:
@objc public func anotherSwiftMethod(parameter: Int = 1) { // Implementation goes here. }
… Будет выглядеть в Objective-C так:
[swiftClassObject anotherSwiftMethodWithParameter:1];
(Здесь 1
- это просто переданное значение, значения по умолчанию не существует.)
Имена
Для взаимодействия языков предусмотрена собственная система мостов. В большинстве случаев это работает удовлетворительно, но иногда требует вмешательства. Например, do(thing:)
превратится в doWithThing:
, что нарушит именование. @objc
снова приходит на помощь:
@objc(doThing:) public func do(thing: Type) { // Implementation goes here. }
Метание
Методы бросания вместо функции throws
имеют дополнительный параметр - указатель NSError
, привязанный к исходному Error
:
@objc(doThing:error:) public func do(thing: Type) throws { // Implementation goes here. }
Использование метода выглядит до боли знакомым:
NSError *error = nil; [swiftClassObject doThing:thingValue error:&error]; if (error) { // Handle error. }
Быстрые типы как аргументы и возвращаемые значения
Если метод имеет параметр не-мостового типа Swift или возвращает его, опять же, его невозможно использовать в Objective-C без дополнительной работы. Рассмотрим исходный код:
public class SwiftClass { public func swiftMethod() { // ... } } public class AnotherSwiftClass { public func anotherSwiftMethod() -> SwiftClass { return SwiftClass() } }
Обертка:
@objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { private let swiftClassObject: SwiftClass // Objective-C exposure is not needed. public init(swiftClassObject: SwiftClass) { self.swiftClassObject = swiftClassObject super.init() } @objc public func swiftMethod() { swiftClassObject.swiftMethod() } } @objc(AnotherSwiftClass) public class AnotherSwiftClassWrapper: NSObject { private let anotherSwiftClassObject = AnotherSwiftClass() @objc public func anotherSwiftMethod() -> SwiftClassObjCWrapper { let object = anotherSwiftClassObject.anotherSwiftMethod() return SwiftClassObjCWrapper(swiftClassObject: object) } }
Сайт для звонков:
AnotherSwiftClass *anotherSwiftClassObject = [AnotherSwiftClass new]; SwiftClass *swiftClassObject = [anotherSwiftClassObject anotherSwiftMethod]; [swiftClassObject swiftMethod];
Протоколы
Нет проблем с открытием протоколов классов Swift, которые не используют не-мостовые типы Swift. Рассмотрим противоположное:
public class SwiftClass { // ... } public protocol SwiftProtocol { func swiftProtocolMethod() -> SwiftClass } public func swiftMethod(swiftProtocolObject: SwiftProtocol) { // Implementation goes here. }
Обертка для класса:
@objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { let swiftClassObject = SwiftClass() }
Обертка для протокола:
@objc(SwiftProtocol) public protocol SwiftProtocolObjCWrapper: class { func swiftProtocolMethod() -> SwiftClassObjCWrapper }
(Мы можем использовать только протоколы классов, дочерние элементы NSObjectProtocol
и протоколы с ограничениями для NSObject
.)
Здесь самое интересное: давайте объявим класс Swift, соответствующий исходному протоколу - это будет что-то вроде моста между исходным протоколом и оболочкой:
class SwiftProtocolWrapper: SwiftProtocol { private let swiftProtocolObject: SwiftProtocolObjCWrapper init(swiftProtocolObject: SwiftProtocolObjCWrapper) { self.swiftProtocolObject = swiftProtocolObject } func swiftProtocolMethod() -> SwiftClass { return swiftProtocolObject .swiftProtocolMethod() .swiftClassObject } }
К сожалению, метод, использующий исходный протокол, тоже должен быть обернут:
@objc public func swiftMethodWith(swiftProtocolObject: SwiftProtocolObjCWrapper) { let swiftProtocolObject = SwiftProtocolWrapper(swiftProtocolObject: swiftProtocolObject) methodOwnerObject.swiftMethodWith(swiftProtocolObject: swiftProtocolObject) }
Не совсем очевидная цепочка. Но если типы и протоколы имеют значительное количество методов, код оболочки не появится в таком непропорциональном объеме.
Строго говоря, протокол, соответствующий Objective-C, довольно чист:
@interface ObjectiveCClass: NSObject<SwiftProtocol> @end @implementation ObjectiveCClass - (SwiftClass *)swiftProtocolMethod { return [SwiftClass new]; } @end
И его использование:
(ObjectiveCClass *)objectiveCClassObject = [ObjectiveCClass new]; [methodOwnerObject swiftMethodWithSwiftProtocolObject:objectiveCClassObject];
Перечисления
При использовании Swift enum
s в Objective-C единственное условие, которое необходимо выполнить, - это то, что они должны иметь целочисленный необработанный тип. Атрибут @objc
можно добавить только после этого.
Если мы не можем изменить существующий enum
, мы можем, как обычно, обернуть его:
enum SwiftEnum { case firstCase case secondCase } class SwiftClass { func swiftMethod() -> SwiftEnum { // Implementation goes here. } } @objc(SwiftEnum) enum SwiftEnumObjCWrapper: Int { case firstCase case secondCase } @objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { let swiftClassObject = SwiftClass() @objc public func swiftMethod() -> SwiftEnumObjCWrapper { switch swiftClassObject.swiftMethod() { case .firstCase: return .firstCase case .secondCase: return .secondCase } } }
Заключение
Конечно, есть и другие аспекты интеграции Swift в Objective-C. Однако я уверен, что со всеми ними можно справиться с помощью описанной выше логики.
Несомненно, у этого подхода есть недостатки. В качестве очевидного примера необходимо написать дополнительный код, часто много кода. Swift явно переведен в среду выполнения Objective-C и работает немного медленнее. Однако во многих случаях это замедление незаметно, особенно на быстрых современных устройствах.
В любом случае обходные пути - это всегда обходные пути. Так что используйте их с умом!
Еще сообщения о шаблонах дизайна:
Если вам нравится читать мои сообщения (и сообщения других авторов) на Medium, вы можете стать полноправным участником Medium (если еще не сделали) здесь.