Совместимый с «Objective-C» «Swift»-код

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

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



Последнее обновление – февраль 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“ и будет работать, скорее всего, уже не так быстро или, по-крайней мере, иначе. Хотя разница во многих случаях невооруженным взглядом заметна не будет.

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

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

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

        Неправда. Bridging header нужен для использования objc-кода в swift'e:
        developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html
          0
          Спасибо, вы правы. Убрал эту дезинформацию.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое