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