О переводе проекта с Objective-C на Swift

Original author: Matt Neuburg
  • Translation
Здравствуйте, уважаемые читатели.

Среди самых животрепещущих тем, которые поднимались на наших издательских советах в последние полгода, особое место занимает язык программирования Swift. При огромном интересе к нему со стороны западных разработчиков и при подлинном изобилии книг на эту тему язык пока кажется довольно сырым. Поэтому, прощупывая почву насчет востребованности нового языка, предлагаем познакомиться с постом великолепного Мэтта Нейбурга, автора книги «Programming iOS 8: Dive Deep into Views, View Controllers, and Frameworks». Автор подробно описывает перевод приложения на новый эппловский язык, убедительно доказывая: «глаза боятся — руки делают», а гибридная сборка Objective-C и Swift отнюдь не напоминает смесь французского с нижегородским.

Приятного прочтения и плодотворных экспериментов.

Если у вас уже есть приложение, написанное на языке Objective-C, попробуйте переписать его на Swift. Это отличная возможность познакомиться с языком Swift, поэкспериментировать со Swift и решить для себя, готовы ли вы избрать Swift в качестве своей основной технологии. Я успел выполнить такую миграцию на нескольких реальных приложениях, в этой статье хочу поделиться некоторыми наблюдениями, почерпнутыми на данном опыте.

Гибридные сборки

Разумеется, вам не придется переводить на Swift сразу весь ваш код; гораздо вероятнее, что вы будете переписывать класс за классом. Как только вы добавите файл Swift в сборку вашего приложения, написанного на Objective-C, эта сборка станет гибридной. Некоторые классы в ней останутся на Objective-C, другие будут написаны на Swift. Соответственно, необходимо сделать так, чтобы все объявления были видимы в коде на обоих языках. Прежде, чем приступить к этой работе, давайте разберемся, как функционирует механизм такой видимости.
Как вы помните, объявление класса на языке Objective-C обычно делится на две части: заголовочный файл (.h), содержащий раздел interface, а также файл с кодом (.m), содержащий раздел @implementation. Если в файле .m требуется информация о классе, он импортирует файл .h этого класса.
Взаимная видимость кода Swift и Objective-C основана на следующем соглашении: она обеспечивается на уровне файлов .h. Существует два направления видимости, каждое из них должно быть рассмотрено отдельно.

Как Swift видит Objective-C

При добавлении файла Swift к сборке на Objective-C или файла Objective-C к сборке на Swift, Xcode предложит вам создать связующий заголовок (bridging header). Это файл .h, входящий в состав проекта. По умолчанию его имя производится от имени сборки, но вообще оно произвольное и может быть изменено, если вы аналогичным образом измените настройку «связующий заголовок» и в сборке Objective-C.
Файл .h на Objective-C будет виден для Swift при условии, что вы импортируете его (#import) в этот связующий заголовок.

Как Objective-C видит Swift

Если у вас есть связующий заголовок, то при создании вашей сборки соответствующие объявления верхнего уровня для всех ваших файлов Swift автоматически переводятся на Objective-C и используются для создания скрытого связующего заголовка в каталоге Intermediates данной целевой сборки, который находится глубоко в папке DerivedData. Этот скрытый заголовок проще всего просмотреть при помощи следующей команды, набираемой в окне терминала:

$ find ~/Library/Developer/Xcode/DerivedData -name "*Swift.h"


Так вы узнаете имя скрытого связующего заголовка. В качестве альтернативы попробуйте просмотреть (или изменить) настройку «Product Module Name» в вашей целевой сборке; имя скрытого связующего заголовка создается на основе указанного здесь имени продукта.

Объявления Swift будут видимы в ваших файлах Objective-C при условии, что вы импортируете (#import) данный скрытый связующий заголовок во все те файлы Objective-C, где должны быть видимы эти файлы Swift.

Ситуация может значительно измениться от того, где именно в верхней части файла .m импортируется скрытый связующий заголовок. Распространенный тревожный сигнал – появление ошибок компиляции “Unknown type name” (Неизвестное имя типа), где неизвестный тип — это класс, объявленный на языке Objective-C. Чтобы решить эту проблему, нужно импортировать файл .h, содержащий объявление неизвестного типа, и в ваши файлы на Objective-C, причем до импорта скрытого связующего заголовка. Такая работа порой раздражает, особенно если тому файлу Objective-C, о котором идет речь, нет необходимости знать об этом классе, но таким образом мы действительно решаем проблему, после чего компиляция может быть продолжена.

Пошаговые инструкции

Прежде, чем вносить какие-либо изменения, создадим новую ветку в git. Теперь переведем наши классы с Objective-C на Swift, по одному. Я буду делать это по следующему алгоритму:

  1. Выберите файл .m, который следует перевести на Swift. Язык Objective-C не может наследовать от класса Swift, поэтому вам придется определить на Objective-C как подкласс, так и сам класс, начиная с подкласса. Класс делегата приложения переводится в последнюю очередь.
  2. Удалите этот файл .m из целевой сборки. Для этого выделите файл .m и воспользуйтесь инспектором файлов.
  3. Во всех файлах Objective-C, которые импортируют соответствующий файл.h, удалите утверждение #import, а на его место импортируйте скрытый связующий заголовок. (Если вы уже импортируете скрытый связующий заголовок в этот файл, то повторять данную процедуру не требуется.)
  4. Если вы импортируете соответствующий файл .h в связующий заголовок, удалите утверждение #import.
  5. Создайте файл .swift для данного класса. Убедитесь, что он добавлен к целевой сборке.
  6. В файле .swift объявите класс и поставьте объявления-заглушки для всех членов, которые были сделаны публичными в файле .h. Если данный класс должен соответствовать протоколам Cocoa, примите их; возможно, вам также понадобится предоставить объявления-заглушки для всех обязательных методов этого протокола. Если этот файл должен ссылаться и на любые другие классы, которые по-прежнему объявлены в вашей целевой сборке на языке Objective-C, импортируйте их .h-файлы в связующий заголовок.
  7. Теперь проект должен скомпилироваться! Разумеется, он не работает, так как в вашем файле .swift еще нет никакого настоящего кода. Но разве это проблема? Итак, пивка для рывка!
  8. Теперь запишем код в нашем файле .swift. Я предпочитаю переводить код из оригинального файла Objective-C построчно, пусть результат получается и не слишком идиоматическим (свифтовским).
  9. Когда код данного файла .m будет полностью переведен на Swift, соберите его, запустите и протестируйте. Если среда исполнения начинает ругаться (тем более, если она при этом валится), что не может найти этот класс, отыщите все ссылки на него в nib-редакторе и повторно введите имя класса в инспекторе идентичности (а также нажмите Tab, чтобы установить изменение). Все сохраните и попробуйте снова.
  10. К следующему .m-файлу! Повторите все вышеуказанные шаги.
  11. Когда все остальные файлы будут переведены, переведите класс делегата приложения. На данном этапе, если в целевой сборке не осталось файлов на Objective-C, то можно удалить файл main.m (заменив его атрибутом @UIApplicationMain attribute in the app delegate class declaration) и файл .pch (предварительно скомпилированный заголовок).


Не думайте, что обязаны переводить весь код на Swift. Вполне возможно, что некоторые разделы будет лучше оставить на Objective-C, это совершенно нормально. На самом деле, некоторый код необходимо оставить на Objective-C, так как в Cocoa API есть детали, к которым Swift не имеет доступа. Например, нельзя написать на Swift функцию C или указатель на функцию, поэтому вы не сможете вызвать CGPatternCreate или AudioServicesAddSystemSoundCompletion без вспомогательного метода Objective-C. Метод appearanceWhenContainedIn: также нельзя вызвать из Swift.

С другой стороны, код, использующий один из методов performSelector:, которые также недоступны на Swift, можно сразу оставить на Objective-C, но рано или поздно вам придется придумать какой-либо обходной маневр, чтобы эти методы можно было заменить кодом на Swift.

Поэкспериментируем со Swift

Поздравляем! Теперь ваше приложение целиком или частично написано на Swift. Но если вы выполняли все мои рекомендации, то на данном этапе приложение еще не слишком «свифтовское». Ведь мы в первую очередь стремились запустить наш код. Теперь, когда все работает, можно вернуться к самому коду и постараться сделать его более идиоматическим. Возможно, вы обнаружите, что какие-то задачи, которые решались на Objective-C заковыристо или неуклюже, гораздо чище и симпатичнее реализуются на Swift

Разумеется, везде, где только можно, следует перейти на нативные типы Swift. Пары неизменяемых / изменяемых типов, например, NSString и NSMutableString, NSArray и NSMutableArray, а также NSDictionary и NSMutableDictionary можно заменить собственными типами Swift: String, Array и Dictionary. Вы также обнаружите, что более не нуждаетесь в некоторых сложных приемах, которые были сопряжены с этими типами. Так, массив обладает методами экземпляра map, filter и reduce; они вам очень пригодятся.

Например, в одном моем приложении был табличный вид, где выводились данные, разделенные по секциям. На внутрисистемном уровне эти данные записаны в массиве массивов, где каждый подмассив состоит из строк, представляющих собой табличные ряды в той или иной секции. В таблице можно выполнять поиск, и теперь я хочу найти и удалить те строки, в которых отсутствует подстрока, введенная пользователем в поле для поиска. Сами секции я трогать не собираюсь, но если при удалении строк какая-то секция полностью опустеет, то я хочу целиком удалить и весь массив данной секции. Вот как это делалось в Objective-C (sb — это UISearchBar):

	NSPredicate* p = [NSPredicate predicateWithBlock:
 
  ^BOOL(id obj, NSDictionary *d) {
 
      NSString* s = obj;
 
      NSStringCompareOptions options = NSCaseInsensitiveSearch;
 
      return ([s rangeOfString:sb.text
 
                       options:options].location != NSNotFound);
 
  }];
 
NSMutableArray* filteredData = [NSMutableArray new];
 
for (NSArray* arr in self.sectionData) {
 
    NSArray* filteredArr = [arr filteredArrayUsingPredicate:p];
 
    if (filteredArr.count)
 
        [filteredData addObject: filteredArr];
 
}
 
self.filteredSectionData = filteredData;


Сначала формируем NSPredicate, чтобы отфильтровать массив. Затем циклически перебираем наш массив массивов, “раздавая” компоненты каждого отфильтрованного подмассива в пустой NSMutableArray, один за другим; уверен, вы знакомы с этой идиомой. В Swift же нам не требуется ни NSPredicate, ни идиомы “раздачи” — равно как и двух промежуточных массивов! У нас есть map и filter, вся работа укладывается в единственное утверждение:

self.filteredSectionData = self.sectionData.map {
 
    $0.filter {
 
        let options = NSStringCompareOptions.CaseInsensitiveSearch
 
        let found = $0.rangeOfString(sb.text, options: options)
 
        return (found != nil)
 
    }
 
}.filter {$0.count > 0}


Вы также заметите, что тех местах, где ваш код опирался на динамику Objective-C, связанную с пересылкой сообщений, вы сможете обойтись без этого механизма, так как в Swift функция является объектом первого класса.

Рассмотрим пример.
В моем приложении со словарными карточками у меня есть класс Term, представляющий латинское слово. Он объявляет множество свойств. Каждой карточке соответствует один термин, а каждое из его свойств отображается в отдельном текстовом поле. Когда пользователь нажимает на любое из текстовых полей на экране, я хочу, чтобы актуальный термин в интерфейсе изменился на следующий – отличающийся от предыдущего тем свойством, которое выбрал пользователь. Соответственно, код для всех трех текстовых полей будет одинаковым; вся разница заключается в том, по какому именно свойству мы будем подбирать следующий термин для вывода на экран.

В Objective-C простейший способ выражения такого параллелизма заключался в использовании пар ключ-значение (g — это распознаватель жестов нажатия):

	NSInteger tag = g.view.tag; // the tag tells us which text field was tapped
 
NSString* key = nil;
 
switch (tag) {
 
    case 1: key = @"lesson"; break; 
 
    case 2: key = @"lessonSection"; break;
 
    case 3: key = @"lessonSectionPartFirstWord"; break;
 
}
 
// получаем актуальное значение соответствующей переменной экземпляра
 
NSString* curValue = [[self currentCardController].term valueForKey: key];


Теперь я могу продолжать использовать пары ключ-значение в Swift; но для этого необходимо, чтобы мой класс Term наследовал от NSObject, а он зависит от динамики Objective-C / Cocoa — перевода строк в имена свойств — чуждой духу Swift. Оказывается, в Swift можно с легкостью реализовать такую же динамику — преобразовав метки в вызовы методов — при помощи простого массива анонимных функций:

	let tag = g.view!.tag
 
let arr : [(Term) -> String] = [
 
    {$0.lesson}, {$0.lessonSection}, {$0.lessonSectionPartFirstWord}
 
]
 
let curValue = arr[tag-1](self.currentCardController.term)


Заключение

Многие свойства Swift нацелены именно на то, чтобы ваш код с самого начала получался более надежным.

Распространенная ошибка при программировании на Objective-C — объявить свойство экземпляра, но забыть установить его в исходное значение; при отправке сообщения к nil ничего не случится, поэтому вы можете довольно долго работать, не замечая проблемы. Swift требует инициализировать все свойства экземпляров. Далее мы подходим к пресловутой строгой типизации Swift; в Objective-C вполне можно гадать, из элементов какого типа состоит массив, но в Swift необходимо указывать для массива строго определенный тип элемента. Не пытайтесь биться с этими чертами Swift, будьте благодарны за них! Если вам попросту удастся заставить ваш код на Swift скомпилироваться, вы уже чему-то научитесь; скорее всего, ваш код будет правильным по той простой причине, что такая корректность заложена в самой природе языка Swift.

Если вы сомневались, стоит ли экспериментировать со Swift – больше не сомневайтесь! Сейчас самое время изучать Swift и смаковать этот язык. В бета-версии среды Xcode 6.3 уже доступна версия Swift 1.2, она позволяет убедиться, что этот язык уже достиг достаточной зрелости. Swift – интересный и простой язык, вы сможете довольно быстро перевести на него любое приложение, которое было написано на Objective-C. Возможно, вы даже удивитесь, насколько яснее и проще окажется получившийся код.

Only registered users can participate in poll. Log in, please.

О востребованности языка Swift

  • 70.2%Нужен перевод вышеупомянутой книги Мэтта Нейбурга, желательно без сокращений и поскорее92
  • 3.0%Нужен перевод другой книги по Swift (ссылка в комментариях)4
  • 26.7%Издавать на русском языке книгу по Swift пока преждевременно35
Издательский дом «Питер»
167.38
Company
Share post

Comments 8

    +2
    Издавать на русском языке книку по Swift пока преждевременно, поскольку на носу WWDC 2015 и наверняка там будут представлены очередные изменения в языке — исправления и синтаксический сахар, и это всё назовут Swift 1.3 (или может скачок будет настолько мощным, что выкатят даже 2.0)

    Большинство книг, изданные на текущий момент, написаны для Swift 1.1, то есть уже на сегодня они могут считаться если не устаревшими, то как минимум — неточными. Выйдут вторые издания, исправленные и дополненные, вот их и можно переводить и выпускать.
      +2
      Благодарю ). Надеюсь, статья понравилась
        0
        В бета-версии среды Xcode 6.3 уже доступна версия Swift 1.2

        Статья писалась давно?
          0
          Это перевод. Оригинальная статья Нойбурга за 3 апреля, на тот момент была выпущена только четвёртая бета Xcode 6.3.
            0
            Опубликована в оригинале 3 апреля
              0
              Ааа, ок.
            +2
            У меня вот есть две более-менее большие программки для себя на Swift. Так вот каждые месяц-два, даже если ничего не трогать, они тупо перестают компилиться из-за очередных изменений в синтаксисе. Так что по-моему рановат этот язык для продакшена…
            • UFO just landed and posted this here

              Only users with full accounts can post comments. Log in, please.