Хоть Apple и написали, казалось бы, подробную документацию о том, как можно использовать «Swift»-код внутри «Objective-C»-приложения (и наоборот), но, когда доходит до дела, этого почему-то окаывается недостаточно. Когда у меня впервые появилась необходимость обеспечить совместимость фреймворка, написанного полностью на «Swift», с «Objective-C»-приложением, документация «Apple» почему-то породила больше вопросов, чем дала ответов (ну или по крайней мере оставила множество пробелов). Интенсивное использование поисковых систем показало, что данная тема освещена в Сети довольно скудно: парочка вопросов на StackOverflow, пара-тройка вводных статей (на англоязычных ресурсах, конечно) – вот и все, что удалось найти.
Данная статья является обощением найденной информации, а также полученного опыта. Подчеркну, она не претендует на то, чтобы называться, как говорится, хорошей практикой, а лишь предлагает возможные действия в описанных обстоятельствах или является неким академическим экспериментом.
Последнее обновление – февраль 2019 г.
Итак, у нас имеется «Objective-C»-проект и некий код на «Swift», который мы хотим использовать в этом проекте. Для примера, пусть это будет сторонний «Swift»-фреймфорк, который мы добавляем в проект, скажем, с помощью «CocoaPods». Как обычно, добавляем нужную зависимость в «Podfile», выполняем
Чтобы импортировать фреймворк в «Objective-C»-файл не нужно ни прописывать
Если вам удалось после импорта «Swift»-заголовка просто использовать какой-либо его класс или метод в «Objective-C»-проекте, вам крупно повезло – значит, кто-то до вас позаботися о совместимности. Дело в том, что «Objective-C» «переваривает» только классы-потомки
Если мы импортируем свой собственный «Swift»-код, то у нас, конечно, есть возможность в нем и «отнаследоваться» от чего угодно, и аннотацию (или атрибут)
Предположим, импортируемый фреймворк содержит следующий нужный нам класс:
Мы создаем свой «Swift»-файл, импортируем в него внешний фреймворк, создаем свой класс, отнаследованный от
(Доступ к классу
По понятным причинам мы не можем использовать те же имена классов и методов в объявлениях. И здесь нам приходит на помощь аннотация
Теперь при вызове из «Objective-C»-кода названия классов и методов будут выглядеть именно так, какими мы хотели бы их видеть – как будто мы пишем соответствующие названия из внешнего класса:
К сожалению, не любые (публичные) «Swift»-методы можно просто пометить
Например, от значений параметров по умолчанию придется отказаться. Такой метод:
…внутри «Objective-C»-кода будет выглядеть так:
(
«Objective-C» обладает своей собственной системой, по которой «Swift»-метод будет назван в среде «Objective-C». В большинстве простых случаев, она вполне удовлетворительная, но зачастую требует нашего вмешательства, чтобы стать удобочитаемой. Например, название метода в духе
Если «Swift»-метод помечен
Использование этого метода будет происходить в духе «Objective-C» (если можно так выразиться):
Если в значениях параметров или возвращаемом значении «Swift»-функции используется не стандартный «Swift»-тип, который не переносится автоматически в среду «Objective-C», этот метод использоваться в среде «Objective-C» опять-таки не выйдет… если над ним не «поколдовать».
Если этот «Swift»-тип является наследником
Обертка для него:
Использование внутри «Objective-C»:
Для примера возьмем, конечно же, протокол, в параметрах или возвратных значениях методов которого используются «Swift»-типы, которые не могут быть использованы в «Objective-C:
Придется снова оборачивать. Для начала –
Далее напишем свой протокол, аналогичный
Далее – самое интересное: объявим „Swift“-класс, адаптирующий нужный нам „Swift“-протокол. Он будет чем-то вроде моста между нашим протоколом, который мы написали для адаптации в „Objective-C“-проекте и „Swift“-методом, который принимает объект исходного „Swift“-протокола. В членах класса будет числиться экземпляр протокола, который мы описали. А методы класса в методах протокола будут вызывать методы написанного нами протокола:
К сожалению, без оборачивания метода, принимающего экземпляр протокола, не обойтись:
Не самая простая цепочка? Да. Хотя, если используемые классы и протоколы обладают ощутимым количеством методов, обертка уже не покажется такой непропорционально-объемной по отношению к исходному коду.
Собственно, использование протокола в самом „Objective-C“-кода будет выглядеть уже вполне гармонично. Реализация методов протокола:
И использование метода:
При использовании перечисляемых типов „Swift“ в „Objective-C“-проектах есть только один нюанс: они должны иметь целочисленный Raw Type. Только после этого мы сможем аннотировать
Что делать, если мы не можем изменить тип
Вот, пожалуй, и все, что я хотел сообщить на данную тему. Скорее всего, есть и другие аспекты интеграции „Swift“-кода в „Objective-C“, но, уверен, с ними вполне можно справиться вооружившись описанной выше логикой.
У данного подхода, конечно, есть и свои минусы. Помимо самого очевидного (написание ощутимого количества дополнительного кода), есть еще один немаловажный: „Swift“-код переносится в среду выполнения „Objective-C“ и будет работать, скорее всего, уже не так быстро или, по-крайней мере, иначе. Хотя разница во многих случаях невооруженным взглядом заметна не будет.
Данная статья является обощением найденной информации, а также полученного опыта. Подчеркну, она не претендует на то, чтобы называться, как говорится, хорошей практикой, а лишь предлагает возможные действия в описанных обстоятельствах или является неким академическим экспериментом.
Последнее обновление – февраль 2019 г.
TL;DR. Чтобы использовать «Swift»-код внутри «Objective-C» придется поступиться некоторыми «фичами» «Swift» и написать обертку над кодом, которая не будет использовать несовместимые с «Objective-C»-приемы («structures», «generics», «enum associated values», «protocol extensions» и пр.), а будет основываться на классах-наследниках NSObject
.
Начало
Итак, у нас имеется «Objective-C»-проект и некий код на «Swift», который мы хотим использовать в этом проекте. Для примера, пусть это будет сторонний «Swift»-фреймфорк, который мы добавляем в проект, скажем, с помощью «CocoaPods». Как обычно, добавляем нужную зависимость в «Podfile», выполняем
pod install
, открываем «xcworkspace»-файл.Чтобы импортировать фреймворк в «Objective-C»-файл не нужно ни прописывать
import
всего фреймворка, как мы привыкли это делать в «Swift», ни пытаться импортировать отдельные файлы публичных API фреймворка, как мы привыкли это делать в «Objective-C». В любой файл, в котором нам необходим доступ к функционалу фреймворка, мы импортируем файл с названием <НазваниеПроекта>-Swift.h
– это автоматически сгенерированный заголовочный файл, который является проводником «Objective-C»-файлов к публичным «API», содержащимся в импортированных «Swift»-файлах. Выглядит это примерно так:#import "YourProjectName-Swift.h"
Использование «Swift»-классов в «Objective-C»-файлах
Если вам удалось после импорта «Swift»-заголовка просто использовать какой-либо его класс или метод в «Objective-C»-проекте, вам крупно повезло – значит, кто-то до вас позаботися о совместимности. Дело в том, что «Objective-C» «переваривает» только классы-потомки
NSObject
и видит только публичные «API». А внутри классов нужные публичные свойства, инициализаторы и методы должны быть аннотированы @objc
.Если мы импортируем свой собственный «Swift»-код, то у нас, конечно, есть возможность в нем и «отнаследоваться» от чего угодно, и аннотацию (или атрибут)
@objc
добавить. Но в таком случае, наверное, у нас есть возможность и нужный код написать на «Objective-C». Поэтому больший смысл имеет сосредоточиться на случае, когда мы хотим импортировать чужой «Swift»-код в свой проект. В этом случае, скорее всего, у нас нет возможности добавить в нужные классы ни какое-либо наследование, ни прочее. Что делать в таком случае? Остается писать обертки!Предположим, импортируемый фреймворк содержит следующий нужный нам класс:
public class SwiftClass {
public func swiftMethod() {
// Implementation goes here.
}
}
Мы создаем свой «Swift»-файл, импортируем в него внешний фреймворк, создаем свой класс, отнаследованный от
NSObject
, а в нем объявляем приватный член типа внешнего класса. Чтобы иметь возможность вызывать методы внешнего класса, мы определяем методы в нашем классе, которые внутри себя будут вызывать соответствующие методы внешнего класса через приватный член класса (звучит запутанно, но по коду, думаю, все понятно):import Foundation
import SwiftFramework
public class SwiftClassObjCWrapper: NSObject {
private let swiftClass = SwiftClass()
public func swiftMethod() {
swiftClass.swiftMethod()
}
}
(Доступ к классу
NSObject
и аннотации @objc
появляется после импорта Foundation.)По понятным причинам мы не можем использовать те же имена классов и методов в объявлениях. И здесь нам приходит на помощь аннотация
@objc
:@objc(SwiftClass)
public class SwiftClassObjCWrapper: NSObject {
private let swiftClass = SwiftClass()
@objc
public func swiftMethod() {
swiftClass.swiftMethod()
}
}
Теперь при вызове из «Objective-C»-кода названия классов и методов будут выглядеть именно так, какими мы хотели бы их видеть – как будто мы пишем соответствующие названия из внешнего класса:
SwiftClass *swiftClass = [SwiftClass new];
[swiftClass swiftMethod];
Особенности использования «Swift»-методов в «Objective-C»-файлах
К сожалению, не любые (публичные) «Swift»-методы можно просто пометить
@objc
и использовать внутри «Objective-C». «Swift» и «Objective-C» – разные языки с разными возможностями и разной логикой, и довольно часто при написании «Swift»-кода мы пользуемся его возможностями, которыми не обладает «Objective-C» или которые реализованы фундаментально по-разному.Например, от значений параметров по умолчанию придется отказаться. Такой метод:
@objc
public func anotherSwiftMethod(parameter: Int = 1) {
// Implementation goes here.
}
…внутри «Objective-C»-кода будет выглядеть так:
[swiftClassObject anotherSwiftMethodWithParameter:1];
(
1
– это переданное нами значение, значение по умолчанию у аргумента отсутствует.)Названия методов
«Objective-C» обладает своей собственной системой, по которой «Swift»-метод будет назван в среде «Objective-C». В большинстве простых случаев, она вполне удовлетворительная, но зачастую требует нашего вмешательства, чтобы стать удобочитаемой. Например, название метода в духе
do(thing:)
«Objective-C» превратит в doWithThing:
, что, возможно, не совпадает с нашим намериением. В этом случае опять-таки приходит на помощь аннотация @objc
:@objc(doThing:)
public func do(thing: Type) {
// Implementation goes here.
}
Методы, выбрасывающие исключения
Если «Swift»-метод помечен
throws
, то «Objective-C» добавит в его сигнатуру еще один параметр – ошибку, которую может выбросить метод. Например:@objc(doThing:error:)
public func do(thing: Type) throws {
// Implementation goes here.
}
Использование этого метода будет происходить в духе «Objective-C» (если можно так выразиться):
NSError *error = nil;
[swiftClassObject doThing:thingValue error:&error];
if (error != nil) {
// Handle error.
}
Использование «Swift»-типов в параметрах и возвращаемых значениях
Если в значениях параметров или возвращаемом значении «Swift»-функции используется не стандартный «Swift»-тип, который не переносится автоматически в среду «Objective-C», этот метод использоваться в среде «Objective-C» опять-таки не выйдет… если над ним не «поколдовать».
Если этот «Swift»-тип является наследником
NSObject
, то, как упоминалось выше, проблем нет. Но чаще всего оказывается, что это не так. В этом случае, нас снова выручает обертка. Например, исходный «Swift»-код:class SwiftClass {
func swiftMethod() {
//
}
}
class AnotherSwiftClass {
func anotherSwiftMethod() -> SwiftClass {
return SwiftClass()
}
}
Обертка для него:
@objc(SwiftClass)
public class SwiftClassObjCWrapper: NSObject {
private let swiftClassObject: SwiftClass
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
func anotherSwiftMethod() -> SwiftClassObjCWrapper {
return SwiftClassObjCWrapper(swiftClassObject: anotherSwiftClassObject.anotherSwiftMethod())
}
}
Использование внутри «Objective-C»:
AnotherSwiftClass *anotherSwiftClassObject = [AnotherSwiftClass new];
SwiftClass *swiftClassObject = [anotherSwiftClassObject anotherSwiftMethod];
[swiftClassObject swiftMethod];
Реализация «Swift»-протоколов «Objective-C»-классами
Для примера возьмем, конечно же, протокол, в параметрах или возвратных значениях методов которого используются «Swift»-типы, которые не могут быть использованы в «Objective-C:
public class SwiftClass { }
public protocol SwiftProtocol {
func swiftProtocolMethod() -> SwiftClass
}
public func swiftMethod(swiftProtocolObject: SwiftProtocol) {
// Implementation goes here.
}
Придется снова оборачивать. Для начала –
SwiftClass
:@objc(SwiftClass)
public class SwiftClassObjCWrapper: NSObject {
let swiftClassObject = SwiftClass()
}
Далее напишем свой протокол, аналогичный
SwiftProtocol
, но использующий обернутые версии классов:@objc(SwiftProtocol)
public protocol SwiftProtocolObjCWrapper {
func swiftProtocolMethod() -> SwiftClassObjCWrapper
}
Далее – самое интересное: объявим „Swift“-класс, адаптирующий нужный нам „Swift“-протокол. Он будет чем-то вроде моста между нашим протоколом, который мы написали для адаптации в „Objective-C“-проекте и „Swift“-методом, который принимает объект исходного „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) {
methodOwnerObject.swiftMethodWith(swiftProtocolObject: SwiftProtocolWrapper(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“ и „Objective-C“
При использовании перечисляемых типов „Swift“ в „Objective-C“-проектах есть только один нюанс: они должны иметь целочисленный Raw Type. Только после этого мы сможем аннотировать
enum
как @objc
.Что делать, если мы не можем изменить тип
enum
, но хотим использовать его внутри „Objective-C“? Мы можем, как обычно, обернуть метод, использующий экземпляры этого перечисляемого типа, и подсунуть ему наш собственный 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“ и будет работать, скорее всего, уже не так быстро или, по-крайней мере, иначе. Хотя разница во многих случаях невооруженным взглядом заметна не будет.