[Из песочницы] Swift-фреймворк в Objective-C-приложении
Хоть 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 и может быть найден здесь. Получившаяся у меня для него обертка — здесь.