[Из песочницы] Swift-фреймворк в Objective-C-приложении

habr.png

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

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

Начало


Для того, чтобы иметь возможность использовать Swift-код внутри Objective-C-проекта в общем случае, необходимо создать так называемый Bridging Header. Самый простой способ достижения этой цели — это добавить в проект любой Swift-файл. После чего (если это первый добавляемый Swift-файл) Xcode сам предложит создать Bridging Header-файл. Соглашаемся — и забываем.

Если момент упущен (т.е. уже имели место попытки добавить Swift-файлы в проект, и Bridging Header создан не был, или он был удален), Bridging Header можно создать и вручную. Ничего сложного в этом нет, и простой запрос в Google «create bridging header manually xcode» первой же ссылкой наверняка приведет к нужной инструкции.

Итак, у нас имеется Objective-C-проект вместе с Bridging Header-файлом и некий код на 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 

@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 и может быть найден здесь. Получившаяся у меня для него обертка — здесь.

© Habrahabr.ru