Данная статья является обощением найденной информации, а также полученного опыта. Подчеркну, она не претендует на то, чтобы называться, как говорится, хорошей практикой, а лишь предлагает возможные действия в описанных обстоятельствах или является неким академическим экспериментом.
Начало
Итак, у нас имеется 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)
pingwinator
02.01.2018 19:39на тему врапера — это тихий ужас. Допустим у меня обж-с приложение, и если я захочу использовать данный сдк, то мне прийдется включать еще и свифт рантайм. +30 метров к весу аппы. Имхо, правильнее написать данный сдк на обж-с с нормальными nullable annotation (и как бонус — лепить свифт обвертку для него — пример facebook skd) или поддерживать 2 паралельные версии на свифте и обж-с (имхо, излишенство, хватит и первого варианта)
hummingbirddj Автор
03.01.2018 21:07Все верно, только изначальные обстоятельства диктовали обратоное.
Ну, предположим, все хотят писать и пишут на Swift. А тут кто-то всплыл с legacy-проектом на Objctive-C. И ему надо тоже эту либу, которая на Swift. И надо примерно сейчас. Писать новую либу на Objective-C? Да, это самое правильное. Да только либа на Swift уже занисает примерно 10 тыс. строк, а на Objective-C займет все 20. На это может не оказаться ресурсов и времени. А потом еще придется тянуть две версии (баги там исправлять, фичи новые добавлять) – тратить на каждую новую задачу в два раза больше времени.
Описанный опыт не претендует на то, чтобы называться хорошей практикой (понятно, что такого плана оберки – это костыли), а просто представляет собой один из путей существования в описанных обстоятельствах. Или, скажем, вовсе академический эксперимент.pingwinator
03.01.2018 21:22так то оно да, но ведь уже была либа на обж-с. Что-то не сходится с начальными условиями.
Имхо, использовать свифт как основной язык для стороней либы до ABI это немного не обдуманый шаг.hummingbirddj Автор
03.01.2018 21:56Это да, действительно либа была раньше на Objective-C, но это, можно сказать, была и не она вовсе – было уже проще написать новую, чем пытаться менять старую. Для новой выбрали Swift с прицелом на будущее и, конечно, соблазнившись скоростью и простотой написания и поддержки – немаловажные плюсы, на мой взгляд. Без ABI – это, безусловно, минус, но пара десятков Мб в современных реалиях – на мой взгляд, не критично. (Telegram X на 40 Мб тяжелее старого приложения еще даже не догнав его по функционалу – кажется, это мало кого волнует. Меня – точно не очень.) Зато Swift и работает потенциально быстрее. В общем, я бы сказал, что у каждого подхода есть свои плюсы и минусы.
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
PapaBubaDiop
Прекрасный набор костылей. Благодарю от имени всей нашей палаты!