Хоть Apple и написали, казалось бы, подробную документацию о том, как можно использовать Swift-код внутри Objective-C-приложения (и наоборот), но когда доходит до дела, этого почему-то окаывается недостаточно. Когда в проекте, в котором я задействован, появилась необходимость обеспечить совместимость Swift-библиотеки одного из продуктов компании с Objective-C-приложением одного из клиентов, документация Apple породила больше вопросов, чем дала ответов (ну или по крайней мере оставила множество пробелов). Интенсивное использование поисковых систем показало, что данная тема освещена в Сети довольно скудно: парочка вопросов на StackOverflow, пара-тройка вводных статей (на англоязычных ресурсах, конечно) – вот и все, что удалось найти.

Данная статья является обощением найденной информации, а также полученного опыта. Подчеркну, она не претендует на то, чтобы называться, как говорится, хорошей практикой, а лишь предлагает возможные действия в описанных обстоятельствах или является неким академическим экспериментом.

Начало


Итак, у нас имеется Objective-C-проект и некий код на Swift, который мы хотим использовать в этом проекте. Для примера, пусть это будет сторонний Swift-фреймфорк, который мы добавляем в проект, скажем, с помощью технологии CocoaPods – скорее всего, это наиболее распространенный случай. Добавим нужную зависимость в Podfile, выполним «pod install/update», откроем полученный (или обновившийся) .xcworkspace-файл – все как обычно.

Чтобы импортировать фреймворк с Objective-C-файл не нужно ни прописывать import всего фреймворка, как мы привыкли это делать в Swift, ни пытаться импортировать отдельные файлы публичных API фреймворка, как мы привыкли это делать в Objective-C. В любой файл, в котором нам необходим доступ к функционалу фреймворка, мы импортируем файл с названием "<НазваниеПроекта>-Swift.h" – это автоматически сгенерированный заголовочный файл, который является проводником Objective-C-файлов к публичным API, содержащимся в импортированных Swift-файлах. Выглядит это примерно так:

#import "YourProjectName-Swift.h"

Использование Swift-классов в Objective-C-файлах


Если вам удалось после импорта Swift-заголовка просто использовать какой-либо Swift-класс или его метод в вашем Objective-C-проекте, вам крупно повезло. Дело в том, что Objective-C «переваривает» только классы-потомки NSObject (любой ступени), либо классы, помеченные @objc.

Если мы импортируем свой собственный Swift-код, то у нас, конечно, есть возможность в нем и «отнаследоваться» от чего угодно, и директиву @objc добавить. Но в таком случае, наверное, у нас есть возможность и нужный код написать на Objective-C. Поэтому больший смысл имеет сосредоточиться на случае, когда мы хотим импортировать чужой Swift-код (скажем, внешний фреймворк) в свой проект. В этом случае, скорее всего, у нас нет возможности добавить в нужные классы ни какое-либо наследование, ни директиву @objc, а автор импортируемого кода вряд ли был настолько любезен, что озаботился об этом специально для нашего случая. Что делать в таком случае? Предлагаемый мной ответ: свой класс-обертка.

Предположим, импортируемый фреймворк содержит следующий нужный нам класс:

public class SwiftClass {

    public func swiftMethod() {
        //
    }

}

Мы создаем свой Swift-файл, импортируем в него внешний фреймворк, создаем свой класс, отнаследованный от NSObject, а в нем объявляем приватный член типа внешнего класса. Чтобы иметь возможность вызывать методы внешнего класса, мы определяем методы в нашем классе, которые внутри себя будут вызывать соответствующие методы внешнего класса через приватный член класса (звучит запутанно, но по коду, думаю, все понятно):

import SwiftFramework

final class _ObjCSwiftClass: NSObject {

    private let swiftClassObject = SwiftClass()

    func _ObjCSwiftMethod() {
        swiftClassObject.swiftMethod()
    }

}

По понятным причинам мы не можем использовать те же имена классов и методов в объявлениях. И здесь нам приходит на помощь директива @objc:

import SwiftFramework

@objc(SwiftClass)
final class _ObjCSwiftClass: NSObject {

    private let swiftClassObject = SwiftClass()

    @objc(swiftMethod)
    func _ObjCSwiftMethod() {
        swiftClassObject.swiftMethod()
    }

}

Теперь при вызове из Objective-C-кода названия классов и методов будут выглядеть именно так, какими мы хотели бы их видеть – как будто мы пишем соответствующие названия из внешнего класса:


SwiftClass *swiftClassObject = [[SwiftClass alloc] init];
[swiftClassObject swiftMethod];

Особенности использования Swift-методов в Objective-C-файлах


К сожалению, не любые Swift-методы можно просто пометить @objc и использовать внутри Objective-C-кода. Swift и Objective-C – разные языки с разными возможностями, и довольно часто при написании Swift-кода мы пользуемся его возможностями, которых нет у Objective-C.

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

func anotherSwiftMethod(withParameter parameterValue: Int = 1) {
    //
}

…внутри Objective-C-кода будет выглядеть так:

[swiftClassObject anotherSwiftMethodWithParameter:1];

(«1» – это переданное нами значение, значения по умолчанию у аргумента отсутствует.)

Названия методов

Objective-C обладает своей собственной системой, по которой Swift-метод будет назван в среде Objective-C. В большинстве простых случаев, она вполне удовлетворительная, но зачастую требует нашего вмешательства, чтобы стать удобочитаемой. Например, название метода в духе do(thing:) Objective-C превратит в doWithThing:, что искажает смысл первоначального имени. В этом случае опять-таки приходит на помощь директива @objc:
@objc(doThing:)
func do(thing: Type) {
    // 
}

Throws-методы

Если Swift-метод помечен throws, то Objective-C добавит в его сигнатуру еще один параметр – ошибку, которую может выбросить метод. Например:

@objc(doThing:error:)
func do(thing: Type) throws {
    //
}

Использование этого метода будет происходить в духе Objective-C (если можно так выразиться):

NSError *error = nil;
[swiftClassObject doThing:thingValue
                    error:&error];
if (error != nil) {
    //
}

Использование типов Swift-классов в параметрах и возвращаемых значений методов

Если в значениях параметров или возвращаемом значении Swift-метода используется не стандартный Swift-тип, который не переносится автоматически в среду Objective-C, этот метод использоваться в среде Objective-C опять-таки не выйдет… если над ним не поколдовать.

Если наш Swift-тип является наследником NSObject (или наследником одного из наследников этого класса, соответственно), то, как упоминалось выше, проблем нет. Но чаще всего оказывается, что это не так. В этом случае нас снова выручает обертка. Например, исходный Swift-код:

class SwiftClass {

    func swiftMethod() {
        //
    }

}

class AnotherSwiftClass {

    func anotherSwiftMethod() -> SwiftClass {
        return SwiftClass()
    }

}

Обертка:

import SwiftFramework

@objc(SwiftClass)
final class _ObjCSwiftClass: NSObject {

    private (set) var swiftClassObject: SwiftClass

    init(swiftClassObject: SwiftClass) {
        self.swiftClassObject = swiftClassObject
    }

    @objc(swiftMethod)
    func swiftMethod() {
        swiftClassObject.swiftMethod()
    }

}

@objc(AnotherSwiftClass)
final class _ObjCAnotherSwiftClass: NSObject {

    private let anotherSwiftClassObject = AnotherSwiftClass()

    @objc(anotherSwiftMethod)
    func anotherSwiftMethod() -> _ObjCSwiftClass {
        return _ObjCSwiftClass(swiftClassObject: anotherSwiftClassObject.anotherSwiftMethod())
    }

}

Использование внутри Objective-C-кода:

AnotherSwiftClass *anotherSwiftClassObject = [[AnotherSwiftClass alloc] init];
SwiftClass *swiftClassObject = [anotherSwiftClassObject anotherSwiftMethod];
[swiftClassObject swiftMethod];

Адаптирование Swift-протоколов Objective-C-классами


Для примера возьмем, конечно же, протокол, в параметрах или возвратных значениях методов которого используются Swift-классы, которые не могут быть использованы в Objective-C-коде.

Предположим: у нас имеется такой класс:

public class SwiftClass {

    //

}

Такой протокол:

public protocol SwiftProtocol {

     func swiftProtocolMethod() -> SwiftClass

}

И какой-нибудь метод, принимающий в параметре объект типа протокола:

public func swiftMethodWith(swiftProtocolObject: SwiftProtocol)

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

Для начала обернем SwiftClass:

@objc(SwiftClass)
final class _ObjCSwiftClass: NSObject {

    let swiftClassObject = SwiftClass()

}

Далее напишем свой протокол, аналогичный SwiftProtocol, но использующий обернутые версии классов:

@objc(SwiftProtocol)
protocol _ObjCSwiftProtocol {

    @objc(swiftProtocolMethod)
    func swiftProtocolMethod() -> _ObjCSwiftClass

}

(Директива @objc перед методом в данном случае нам понадобится только, если нас не устроит название, которое Objective-C даст методу автоматически.)

Далее – самое интересное: объявим Swift-класс, адаптирующий нужный нам Swift-протокол. Он будет чем-то вроде моста между нашим протоколом, который мы написали для адаптации в Objective-C-проекте и Swift-методом, который принимает объект исходного Swift-протокола. Членом класса будет выступать экземпляр протокола, который мы описали. А методы класса в методах протокола будут вызывать методы написанного нами протокола:

final class SwiftProtocolWrapper: SwiftProtocol {

    private (set) var swiftProtocolObject: _ObjCSwiftProtocol

    init(swiftProtocolObject: _ObjCSwiftProtocol) {
        self.swiftProtocolObject = swiftProtocolObject
    }

    func swiftProtocolMethod() -> SwiftClass {
        return swiftProtocolObject.swiftProtocolMethod().swiftClassObject
    }

}

К сожалению, без оборачивания метода, принимающего экземпляр протокола, не обойтись:

@objc
func swiftMethodWith(swiftProtocolObject: _ObjCSwiftProtocol) {
    methodOwnerObject
        .swiftMethodWith(swiftProtocolObject: SwiftProtocolWrapper(swiftProtocolObject: swiftProtocolObject))
}

(В данном случае после директивы @objc нет необходимости обозначать желаемое имя для метода – именно это имя сохранит в Objective-C свой первоначальный вид.)

Не самая простая цепочка? Да. Хотя, если используемые классы и протоколы обладают ощутимым количеством методов, обертка уже не покажется такой непропорционально объемной по отношению к нужному нам Swift-коду.

Собственно, использование протокола в самом Objective-C-кода будет выглядеть уже вполне гармонично. Реализация методов протокола:

@interface ObjectiveCClass: NSObject <SwiftProtocol>

@end

@implementation ObjectiveCClass

- (SwiftClass *)swiftProtocolMethod {
    return [[SwiftClass alloc] init];
}

@end

И использование метода:

(ObjectiveCClass *)objectiveCClassObject = [[ObjectiveCClass alloc] init];
[methodOwnerObject swiftMethodWithSwiftProtocolObject:objectiveCClassObject];

Перечисляемые типы в Swift и Objective-C


При использовании перечисляемых типов Swift в Objective-C-проектах есть только один нюанс: они должны быть типа Int. Только после этого мы сможем отметить enum как @objc.

Что делать, если мы не можем изменить тип enum, но хотим использовать его в нашем Objective-C-проекте? Мы можем, как обычно, обернуть метод, использующий экземпляры этого перечислимого типа, и подсунуть ему наш собственный enum. Например, Swift enum:

enum SwiftEnum {
    case FirstCase
    case SecondCase
}

Swift-метод, его использующий:

final class SwiftClass {

    func swiftMethod() -> SwiftEnum {
        //
    }

}

Наш enum:

@objc(SwiftEnum)
enum _ObjCSwiftEnum: Int {
    case FirstCase
    case SecondCase
}

Обертка для SwiftClass и swiftMethod():

@objc(SwiftClass)
final class _ObjCSwiftClass: NSObject {

    let swiftClassObject = SwiftClass()

    @objc
    func swiftMethod() -> _ObjCSwiftEnum {
        switch swiftClassObject.swiftMethod() {
        case .FirstCase:
            return .FirstCase
        case .SecondCase:
            return .SecondCase
        }
    }

}

Заключение и бонус


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

У данного подхода, конечно, есть и свои минусы. Помимо самого очевидного (написание ощутимого количества дополнительного кода), есть еще один немаловажный: Swift-код переносится в среду выполнения Objective-C и будет работать, скорее всего, уже не так быстро. Хотя и разница, конечно, во многих случаях невооруженным взглядом заметна не будет.

Бонус

Проект, в рамках работы над которым у меня возникла необходимость придумать способ его использования в рамках Objective-C-приложения имеет статус open source и может быть найден здесь. Получившаяся у меня для него обертка – здесь.

Комментарии (7)


  1. PapaBubaDiop
    02.01.2018 00:48

    Прекрасный набор костылей. Благодарю от имени всей нашей палаты!


  1. pingwinator
    02.01.2018 19:39

    на тему врапера — это тихий ужас. Допустим у меня обж-с приложение, и если я захочу использовать данный сдк, то мне прийдется включать еще и свифт рантайм. +30 метров к весу аппы. Имхо, правильнее написать данный сдк на обж-с с нормальными nullable annotation (и как бонус — лепить свифт обвертку для него — пример facebook skd) или поддерживать 2 паралельные версии на свифте и обж-с (имхо, излишенство, хватит и первого варианта)


    1. hummingbirddj Автор
      03.01.2018 21:07

      Все верно, только изначальные обстоятельства диктовали обратоное.
      Ну, предположим, все хотят писать и пишут на Swift. А тут кто-то всплыл с legacy-проектом на Objctive-C. И ему надо тоже эту либу, которая на Swift. И надо примерно сейчас. Писать новую либу на Objective-C? Да, это самое правильное. Да только либа на Swift уже занисает примерно 10 тыс. строк, а на Objective-C займет все 20. На это может не оказаться ресурсов и времени. А потом еще придется тянуть две версии (баги там исправлять, фичи новые добавлять) – тратить на каждую новую задачу в два раза больше времени.

      Описанный опыт не претендует на то, чтобы называться хорошей практикой (понятно, что такого плана оберки – это костыли), а просто представляет собой один из путей существования в описанных обстоятельствах. Или, скажем, вовсе академический эксперимент.


      1. pingwinator
        03.01.2018 21:22

        так то оно да, но ведь уже была либа на обж-с. Что-то не сходится с начальными условиями.
        Имхо, использовать свифт как основной язык для стороней либы до ABI это немного не обдуманый шаг.


        1. hummingbirddj Автор
          03.01.2018 21:56

          Это да, действительно либа была раньше на Objective-C, но это, можно сказать, была и не она вовсе – было уже проще написать новую, чем пытаться менять старую. Для новой выбрали Swift с прицелом на будущее и, конечно, соблазнившись скоростью и простотой написания и поддержки – немаловажные плюсы, на мой взгляд. Без ABI – это, безусловно, минус, но пара десятков Мб в современных реалиях – на мой взгляд, не критично. (Telegram X на 40 Мб тяжелее старого приложения еще даже не догнав его по функционалу – кажется, это мало кого волнует. Меня – точно не очень.) Зато Swift и работает потенциально быстрее. В общем, я бы сказал, что у каждого подхода есть свои плюсы и минусы.


  1. PoltoraIvana
    03.01.2018 20:57

    Для того, чтобы иметь возможность использовать Swift-код внутри Objective-C-проекта в общем случае, необходимо создать так называемый Bridging Header.

    Неправда. Bridging header нужен для использования objc-кода в swift'e:
    developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html


    1. hummingbirddj Автор
      03.01.2018 21:14

      Спасибо, вы правы. Убрал эту дезинформацию.