
Несмотря на то, что 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 enums в 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 (если еще не сделали) здесь.