Pull to refresh

Переходим на Swift 3 с помощью миграционного «робота» в Xcode 8.1 и 8.2

Reading time 14 min
Views 13K


Уже известно, что Xcode 8.2 будет последним релизом, который поддерживает переходную версию Swift 2.3. Поэтому нужно срочно подумать о миграции на Swift 3.

Я хочу поделиться некоторым опытом такой миграции на примере приложений, связанных со стэнфордским курсом «Developing iOS 9 Apps with Swift», как демонстрационных (их 12), так и полученных в результате выполнения Заданий этого обучающего курса (их 6 с вариантами). Они все разной сложности, но там есть и рисование, и многопоточность, и показ изображений с помощью ScrollView, и работа с сервером Twitter, и база данных Core Data, и работа с облачным сервисом Cloud Kit, и карты Map Kit. И все это было написано на Swift 2.2 (stanford.edu), а мне было необходимо перевести все приложения на Swift 3. Конспект лекций стэнфордского курса на русском языке можно найти на сайте «О стэнфордских лекциях», а код — для Swift 2.3 на Github и для Swift 3 на Github.

Если вы решили мигрировать на Swift 3, то в Xcode 8 вам нужно запустить инструмент миграции (своебразного «робота») c помощью меню EditConvertto Current Swift Syntax:


Далее вам предлагают карту различий между исходным кодом Swift 2 и кодом Swift 3, который сгенерировал этот «робот»:



Надо сказать, что миграционный «робот» в Xcode 8.1 и Xcode 8.2 работает превосходно по сравнению с начальной версией в Xcode 8.0, с которой мне пришлось начинать. Новый миграционный «робот» — изобретательный и очень разумный. На примере этой карты различий можно прекрасно изучать, какие изменения претерпели те или иные синтаксические конструкции в Swift 3. Миграционный «робот» делает очень большую работу по замене имен, сигнатуры методов и свойств, превращая, если это необходимо, ранее обычные свойства в Generic (например, NSFetchRequest, который не является Generic в Swift 2, но является таковым в Swift 3). Он может заменять новым кодом целые «паттерны», например, синглтон, если он был выполнен старыми средствами с помощью dispatch_once(&onceToken).

Ниже я покажу примеры этого. Миграционный «робот» действует по принципу «не навреди» и где только можно старается поддержать работоспособность имеющегося кода, даже вставляя дополнительный код. Вам следует очень внимательно посмотреть эти изменения и включить в список те места, код которых вам непонятен или кажется неэффективным и менее читаемым. Назовем это списком заданий для уточнения кода.

Если вы согласны с предложенными «роботом» преобразованиями, то вы их сохраняете и работаете дальше. Но, как и ожидалось, миграционный «робот» делает только часть работы для получения компилируемого кода в Swift 3. В приложениях остается 2-3 ошибки и 3-4 предупреждения. Поэтому вашим следующим шагом будет открытие навигатора «ошибок и предупреждений» (если они есть) и исследование их все одного за другим:



Для большинства ошибок и предупреждений предлагаются способы решения, и, в основном, это правильные решения:



Нам нужно сделать «кастинг типа» для переменной json, которая в Swift 3 представлена «роботом» как Any, хотя мы работаем с ней как с ссылочной (reference) переменной. В результате получаем:


Но иногда приходится исправлять ошибки. Сразу после работы миграционного «робота» мы имеем ошибку при инициализации «параллельной» очереди (более подробно этот случай рассматривается ниже):



Вместо двух строк с ошибкой добавляем одну строку с правильным кодом:



Иногда вам предлагается несколько вариантов решения проблемы, и вы можете выбрать любой:


Нам сообщается, что неявно произойдет принудительное преобразование String? в Any. Это можно исправить тремя способами и тем самым убрать это предупреждение:

  1. предоставить значение по умолчанию,
  2. принудительно «развернуть» Optional значение,
  3. осуществить явный «кастинг» в Any с помощью кода as Any.

Мы предпочтем первый вариант и будем использовать пустую строку " ", если выражение равно nil:


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

В результате работы со списком ошибок и предупреждений вам удалось откомпилировать приложение и запустить тестовый пример. Это очень важный этап миграции.

Теперь вы можете сфокусироваться на списке заданий на уточнение кода, который вы составили при просмотре карты различий между двумя версиями: Swift 2 и Swift 3. Весь этот код технически корректен, но он может быть либо избыточным, либо неэффективным, либо приводить к ошибкам выполнения. Некоторые из этих ситуаций имеют общий характер, а некоторые — сильно зависят от специфики вашего приложения.

Я поделюсь некоторыми из них, с которыми мне пришлось столкнутся при миграции приложений курса «Developing iOS 9 Apps with Swift».

1. Нужно вернуть уровень доступа fileprivate обратно в private.


В процессе миграции на Swift 3 все уровни доступа private заменяются на новый уровень доступа fileprivate, потому что private в Swift 2 имел смысл именно fileprvate.



Миграционный «робот» действует по принципу «не навреди», поэтому он заменил все старые private на новые fileprivate, то есть расширил область доступа private переменных и методов.


В большинстве случаев это излишняя предосторожность, и нам вовсе не нужен уровень доступа fileprivate, но вы должны решить это самостоятельно в своей команде разработчиков и исправить в ручном режиме.

Если вы разрабатываете framework, то миграционный «робот» в Swift 3 заменит все уровни доступа public, которые были в Swift 2, на новый уровень доступа open. Это касается только классов.



В Swift 3:

  • open класс доступен и может иметь subclasses за пределами модуля, в котором он определен. Свойства и методы open класса доступны и могут быть переопределены (overridable) за пределами модуля, в котором определен класс.

  • public класс доступен, но не может иметь subclasses за пределами модуля, в котором он определен. Свойства и методы public класса доступны, но не могут быть переопределены (overridable) за пределами модуля, в котором определен класс.

Таким образом, уровень доступа open — это то, что было public в предыдущих версиях Swift, а уровень доступа public более ограничен. Крис Латтнер сказал в SE-0177: Allow distinguishing between public access and public overridability, что в Swift 3 уровень доступа open просто более public, чем public. Еще можно посмотреть SE-0025 Scoped Access Level.


При миграции frameworks в Swift 3 мы не будем возвращать уровень доступа open назад в public. Здесь нас все устраивает.

Вообще иерархия уровней доступа в Swift 3 так располагается в порядке убывания:

openpublicinternalfileprivateprivate

2. Вы не можете сравнивать Optional значения в Swift 3.


При автоматической миграции с Swift 2 на Swift 3 иногда перед некоторыми классами появляется код:


Дело в том, что в Swift 2 можно было сравнивать Optional значения, например, таким образом:


или так:


В Swift 3 такую возможность убрали (SE-0121 – Remove Optional Comparison Operators) и для сохранения работоспособности такого кода в Swift 3 миграционный «робот» добавляет вышеприведенный код, что, конечно, удобно на начальном этапе перехода на Swift 3, но некрасиво, так как если у вас встречается сравнение Optional значений в нескольких расположенных в отдельных файлах классах, то вышеприведенный код добавиться многократно. Этот код нужно удалить, сразу обозначится проблема, и решать проблему нужно на месте. Вначале избавляемся от Optional с помощью синтаксической конструкции if let, а затем проводим необходимое сравнение. Например, так:


или так:


3. Swift 3 не обеспечивает автоматической совместимости (bridging) чисел с NSNumber.


В Swift 2 многие типы при необходимости автоматически совмещались («bridging«) с экземплярами некоторых subclasses NSObject, например, String в NSString, или Int, Float, … в NSNumber. В Swift 3 вам придется делать это преобразование явно (SE -0072 Fully eliminate implicit bridging conversions from Swift). Например, в Swift 2 мы имели код для преобразования числа в строку:


В Swift 3 после миграционного «робота» мы получим ошибку:


От нас требуют явного преобразования Double в NSNumber, и мы можем использовать два способа преобразования — с помощью оператора as:


или с помощью инициализатора NSNumber:


4. «Робот» не умеет преобразовывать параллельные очереди, но прекрасно работает с dispatch_once


Например, обычный «паттерн» асинхронного выполнения кода на параллельной очереди QOS_CLASS_USER_INITIATED с последующим переходом на main queue для отображения данных на UI на Swift 2 выглядит следующим образом:


Миграционный робот" преобразует этот код в код с ошибкой и предлагает функцию global(priority: qos), которая будет упразднена в iOS 10:


Для того, чтобы убрать эту ошибку, нам нужно использовать другую функцию — global (qos: .userInitiated):


Зато миграционный «робот» прекрасно справляется с dispatch_once, которая упразднена в Swift 3, и ее следует заменить либо глобальной, либо статической переменной или константой.

Вот как выглядит код для однократной инициализации фоновой очереди при выборке данных с сервера Flickr.com в Swift 2:



А это код в Swift 3 после работы миграционного «робота»:



Вы видите, что «робот» вынул внутренность синглтона и оформил ее в виде lazy переменной __once, которая представлена как выполняемое замыкание, при это нас предупреждают, что переменная onceToken не используется. Она действительно больше не нужна, и мы убираем эту строку:



5. Будьте очень внимательны с заменой методов типа …inPlace, при переходе на Swift 3.


Swift 3 возвращается к соглашению о наименовании методов и функций, которое было в Swift 1, то есть функции и методы именуются в зависимости от того, создают ли они «побочный эффект». И это замечательно. Давайте приведем пару примеров.

Вначале рассмотрим методы не имеющие «побочного эффекта», они, как правило, именуются Существительными. Например,

x.distance (to: y)
x = y.union(z)


Если функции и методы имеют «побочный эффект», то они, как правило, именуются императивным Глаголом в повелительном наклонении. Если я хочу, чтобы массив X был отсортирован, то я скажу: «X отсортируй (sort) сам себя или X добавь (append) к себе Y»:

x.sort ()
x.append(y)
y.formUnion(z)


Таким образом Swift 3 группирует методы по двум категориям: методы, которые производят действие по месту — думайте о них как о Глаголах — и методы, которые возвращают результат выполнения определенного действия, не затрагивая исходный объект — думайте о них как о Существительных.
Если НЕТ окончания «ed», то все происходит «по месту»: sort (), reverse (), enumerate (). Это Глаголы. Каждый раз, когда Swift 3 модифицирует метод добавлением окончания «ed» или «ing»: sorted (), reversed (), enumerated (), то мы имеем возвращаемое значение. Это Существительные.

Эти довольно невинные правила вызывают путаницу, если речь заходит об изменении методов сортировки при переходе от Swift 2 к Swift 3. Дело в том, что в Swift 2 все функции и методы, которые работают «по месту», содержат в своем названии слово «InPlace», поэтому для сортировки по месту используется функция sortInPlace (), а функция sort () в Swift 2 возвращает отсортированный массив. В Swift 3, как видно из вышеприведенных примеров, sort () переименован в sorted (), а sortInPlace () в sort ().

В результате метод sort () имеет разную семантику в Swift 2 и в Swift 3. Но это нестрашно, потому что если и в Swift 2, и в Swift 3 имеется пара функций ( как с побочным эффектом, так и без него), то миграционный «робот» блестяще осуществит замену одного имени другим:



А что, если в Swift 2 были две функции, а в Swift 3 осталась одна? Например, в Swift 2 были функции insetInPlace и insetBy, а в Swift 3 осталась, по какой-то причине, одна — insetBy? Миграционный «робот» нам в этом случае не поможет — он оставит старое название функции — insetInPlace — которое, конечно, даст ошибку, и нам придется исправлять ее вручную.



Все методы в Swift 2 с присутствием «inPlace» в имени требуют особого внимания при переходе на Swift 3.

Я сама попалась на этом вроде бы невинном изменении. Рассмотрим простейший метод one(), который увеличивает размер прямоугольника bbox до тех пор, пока не «поглотит» некий другой прямоугольник rect. Этот сильно упрощенный пример имеет реальный прототип, а именно класс AxesDrawer, который был предоставлен в стэнфордском курсе для рисования осей графика в Задании 3. Именно там встречается случай, представленный ниже и с ним пришлось иметь дело при переводе класса AxesDrawer из Swift 2.3 в Swift 3.


В Swift 2 я могу использовать метод insetInPlace для прямоугольников CGRect, который будет увеличивать размер прямоугольника на dx по оси X и на dy по оси Y:


Здесь не требуется использовать возвращаемое значение метода insetInPlace, потому что прямоугольник изменяется «по месту».

Если мы используем миграционный «робот» для перехода на Swift 3, то он оставит метод insetInPlace неизменным, так как аналога ему в Swift 3 нет, и мы получим ошибку:


В Swift 3 есть только метод insetBy, применяем его, ошибка исчезает, и нам предлагают изменить переменную var bbox на константу let bbox:


что мы и делаем:


Вы видите, что нет никаких предупреждений, никаких ошибок, а мы ведь создали «вечный» цикл, потому что новый метод insetBy не изменяет прямоугольник «по месту», а возвращает измененное значение, которое мы не используем в цикле while, но об этом тоже почему-то нет сообщения, так что создалась ОЧЕНЬ ОПАСНАЯ ситуация, когда мы «зациклили» навсегда наш код.

Мы должны снова присвоить bbox, возвращаемое методом insetBy значение:


Естественно, нам предлагают обратно вернуться от константы let bbox к переменной var bbox, и мы это делаем:


Теперь код работает правильно. Так что будьте очень внимательны с заменой методов …inPlace при переходе на Swift 3.

6. В Swift 3 запрос NSFetchRequest <NSFetchRequestResult> к базе данных Core Data стал Generic


Но работоспособность класса CoreDataTableViewController, предоставленного стэнфордским университетом для работы с данными Core Data в таблице, обеспечивается автоматически при использовании миграционного инструмента. Давайте рассмотрим, как это получается.

Если вы работаете с фреймворком Core Data, то следует обратить внимание на то, что запрос к базе данных, который в Swift 2 был NSFetchRequest, в Swift 3 стал Generic NSFetchRequest <NSFetchRequestResult>, а следовательно, стал Generic и класс NSFetchResultsController<NSFetchRequestResult>. В Swift 3 они стали зависеть от выбираемого результата, который должен реализовать протокол NSFetchRequestResult:


К счастью, объекты NSManagedObject базы данных Core Data автоматически выполняют протокол NSFetchRequestResult и мы «законно» можем рассматривать их в качестве результата запроса.

В Swift 2 запрос и его выполнение выглядят так:


В Swift 3 мы можем указать в запросе тип получаемого результата (в нашем случае Photo), и тем самым избежать дополнительного «кастинга типа»:


Действительно, если мы посмотрим на тип результата выборки results в Swift 3, то это будет [Photo], что нам позволит извлечь атрибут unique объекта базы данных Photo:


Однако, если бы мы использовали миграционный «робот» для перехода на Swift 3, то мы получили бы код, в котором результат выборки results определяется только тем, что он должен выполнять протокол NSFetchRequestResult:


Поэтому «роботу» пришлось применить «кастинг типа» as ? [Photo] для извлечения атрибута unique объекта базы данных Photo. Мы видим, что миграционный «робот» опять пытается нам «подсунуть» более обобщенное решение, вполне работоспособное, но менее эффективное и менее «читабельное», чем приведенный выше «ручной» вариант. Поэтому после работы миграционного «робота» нам придется править код вручную.

Но есть одно место в приложениях, связанных с Core Data, где миграционный «робот», работая так, как показано выше, предлагает гениальный код в Swift 3. Это класс NSFetchResultsController, который в Swift 3 также, как и запрос NSFetchRequest стал Generic, то есть NSFetchResultsController<NSFetchRequestResult>. В результате возникли некоторые трудности при использовании в Swift 3 фантастически удобного класса CoreDataTableViewController, который разработан в Стэнфорде.

Вначале очень кратко напомню о том, откуда появился класс CoreDataTableViewController. Когда у вас огромное количество информации в базе данных, то прекрасным средством показа этой информации является Table View. В 99% случаев либо Table View, либо Collection View используются для показа содержимого больших баз данных. И это настолько распространено, что Apple обеспечила нас в iOS прекрасным классом NSFetchedResultsController, который “подвязывает” запрос NSFetchRequest к таблице UITableView.

И не только “подвязывает” лишь однажды, а эта “подвязка” действует постоянно и, если в базе данных каким-то образом происходят изменения, NSFetchRequest возвращает новые результаты и таблица обновляется. Так что база данных может меняться “за сценой”, но таблица UITableView всегда остается в синхронизированном с ней состоянии.

NSFetchResultsController обеспечивает нас методами протоколов UITableViewDataSource и UITableViewDelegate, такими, как numberOfSectionsInTableView, numberOfRowsInSections и т.д. Единственный метод, который он не реализует, — это cellForRowAt. Вам самим придется реализовать его, потому что для реализации метода cellForRowAt нужно знать пользовательский UI для ячейки таблицы, а вы — единственный, кто знает, какие данные и как они размещаются на экране. Но что касается других методов протокола UITableViewDataSource, даже таких, как sectionHeaders и всего остального, NSFetchedResultsController берет все на себя.

Как работать с NSFetchResultsController?

От вас потребуется только создать запрос request, настроить его предикат и сортировку, а выводом данных в таблицу займется NSFetchResultsController.

NSFetchResultsController также наблюдает за всеми изменениями, происходящими в базе данных, и синхронизирует их с Table View.

Способ, каким она это делает, связан с делегатом NSFetchResultsControllerDelegate, методы которого вам предлагается без изменения скопировать из документации в ваш класс.

«Ну вот, я думал, что настроить NSFetchResultsController — это просто, а тут выясняется, что я должен реализовать методы делегата NSFetchResultsControllerDelegate?» — подумаете вы.
Но вам повезло, всю эту работу проделали за вас и предоставили в ваше распоряжение замечательный класс с именем CoreDataTableViewController.

При этом был не только скопировал весь необходимый код из документации по NSFetchResultsController, но и переписан с Objective-C на Swift.

Теперь, для того, чтобы ваш UITableViewController унаследовал всю функциональность NSFetchResultsController, вам достаточно сделать CoreDataTableViewController вашим superclass и определить public var с именем fetchedResultsController. Вы устанавливаете эту переменную, и CoreDataTableViewController будет использовать ее для ответа на все вопросы UITableViewDataSource, а также делегата NSFetchedResultsController, который будут отслеживать изменение базы данных.

В итоге вам всего лишь нужно:

  1. установить переменную var fetchedResultsController и
  2. реализовать метод cellForRowAt.

В классе, наследующим от CoreDataTableViewController, cоздаем NSFetchResultsController с помощью инициализатора, включающего в качестве аргумента запрос request, а затем присваиваем его переменной var с именем fetchedResultsController. Как только вы это сделаете, таблица cо списком фотографий начнет автоматически обновляться (Swift 2):


Конечно, реализуем метод cellForRowAtIndexPath (Swift 2):


Получаем список фотографий с сервера Flickr.com:


Все очень здорово и просто в Swift 2, но в Swift 3 запрос NSFetchRequest<NSFetchRequestResult> стал Generic, а следовательно, стал Generic и класс NSFetchResultsController<NSFetchRequestResult&gt.

Переменная public var с именем fetchedResultsController, с которой мы работаем в CoreDataTableViewController, тоже стала Generic в Swift 3 после применения миграционного «робота»:


По идее и класс CoreDataTableViewController нужно сделать Generic, но мы этого делать не будем, потому что его subclasses, например, такие, как приведенный выше PhotosCDTVC, испольуются на storyboard, а на storyboard не работают Generic классы.

Как же нам быть? Класс CoreDataTableViewController чрезвычайно удобный и позволяет избежать дублирования кода во всех Table View, работающих c Core Data?

Тут нам на помощь приходит миграционный «робот». Посмотрите, как он преобразовал класс PhotosCDTVC в части определения переменной с именем fetchedResultsController, в которой результат выборки в запросе определяется только тем, что он должен выполнять протокол NSFetchRequestResult (Swift 3):


А это как раз то, что требует переменная с именем fetchedResultsController в нашем суперклассе CoreDataTableViewController, то есть фактически «робот» выполнил «кастинг типа» ВВЕРХ (upcast) нашего результата выборки объекта базы данных Photo до NSFetchRequestResult. Понятно, что мы получим результат выборки типа NSFetchRequestResult, поэтому когда приходит время работать с реальным объектом Photo в методе cellForRowAt миграционный «робот» выполняет обратную операцию — «кастинг типа» ВНИЗ (downcast) — с помощью оператора as? (Swift 3):


Так что в случае с классом CoreDataTableViewController миграционный «робот» сработал идеально. Вам ничего не нужно изменять или дополнять.

7. Swift 3 расширил использование синтаксиса #selector, аргументами могут быть getter: и setter: для Objective-C свойств.


Когда вы определяете в Swift 3 селектор #selector, относящийся к Objective-C свойствам, то необходимо указать, имеете ли вы ввиду setter или getter.

Так получилось, что в одном из своих приложений на Swift, работающих с Core Data, я использовала в качестве public API переменную var coreDataStack:


Эту переменную я устанавливаю в AppDelegate не совсем обычным образом — через Objective-C setter setCoreDataStack для Swift свойства с именем coreDataStack. Этот способ я подсмотрела на одном из видео на сайте raywenderlich.com:


Мне было любопытно, как можно установить селектор на метод setCoreDataStack, которого явно нет в приложении. Этот код так и остался, пока я не решила перейти на Swift 3. Какого же было мое удивление, когда я обнаружила, как деликатно обошелся с этим кодом миграционный «робот» — он использовал синтаксическую конструкцию #selector с незнакомым для меня аргументом setter:


Мне захотелось больше узнать о #selector и я нашла замечательную статью «Hannibal #selector».

8. В Swift 3 вы получите предупреждение, если не будете использовать возвращаемое функцией не Void значение.


В Swift 2 при вызове функции необязательно было использовать возвращаемое функцией значение, даже если это значение не Void. Никакого предупреждения от компилятора в этом случае не поступало. Если вы хотите, чтобы пользователь получал такое предупреждение от компилятора, то вам нужно было специально разместить предложение @warn_unused_result перед декларированием этой функции. Это касалось, в основном, методов, которые меняют структуру данных. Например, sortInPlace.

В Swift 3 ситуация поменялась на противоположную. Теперь всегда, когда вы не используете любую функцию с возвращаемым значением, вы будете получать предупреждение. Для того, чтобы отменить в Swift 3 появление такого предупреждения достаточно разместить предложение @discardableResult перед декларацией функции.

Например, в Swift 2 мы могли использовать метод без получения возвращаемого значения:


Но после применения миграционного «робота» вы получите в этом коде предупреждение:


Которое сообщает вам, что возвращаемое значение [UIViewController]? не используется. Если вы хотите убрать это предупреждение, то нужно дать понять компилятору ЯВНО, что вы не интересуетесь возвращаемым значение, с помощью символа _ (подчеркивания):



ВЫВОДЫ


Перевод кода из Swift 2 на Swift 3 — очень увлекательное занятие. Можно в качестве исходных файлов использовать те, которые указаны в начале поста, а можно и более ранние, написанные. например, на Swift 2.0. Так что используйте миграционный «робот» в Xcode 8.1 и 8.2 для расширения своих знаний о Swift 3. Если вы хотите использовать в вашем приложении, написанном на Swift 3, какие-то куски кода, написанного на Swift 2, то также удобно использовать миграционный «робот». Надеюсь, он вас не подведет.

Ссылки: Yammer iOS App ported to Swift 3
Tags:
Hubs:
+21
Comments 32
Comments Comments 32

Articles