Переходим на Swift 3 с помощью миграционного «робота» в Xcode 8.1 и 8.2
Уже известно, что 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 помощью меню Edit → Convert → to 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 . Это можно исправить тремя способами и тем самым убрать это предупреждение:
- предоставить значение по умолчанию,
- принудительно «развернуть» Optional значение,
- осуществить явный «кастинг» в Any с помощью кода as Any .
Вообще миграция — это прекрасный повод для того, чтобы более широко взглянуть на свой код и, возможно, улучшить его.
В результате работы со списком ошибок и предупреждений вам удалось откомпилировать приложение и запустить тестовый пример. Это очень важный этап миграции.
Теперь вы можете сфокусироваться на списке заданий на уточнение кода, который вы составили при просмотре карты различий между двумя версиями: 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 . Это касается только классов.
-
open класс доступен и может иметь subclasses за пределами модуля, в котором он определен. Свойства и методы open класса доступны и могут быть переопределены (overridable) за пределами модуля, в котором определен класс.
При миграции frameworks в Swift 3 мы не будем возвращать уровень доступа open назад в public . Здесь нас все устраивает.
Вообще иерархия уровней доступа в Swift 3 так располагается в порядке убывания:
open → public → internal → fileprivate → private
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 , который будут отслеживать изменение базы данных.
В итоге вам всего лишь нужно:
- установить переменную var fetchedResultsController и
- реализовать метод cellForRowAt .
Конечно, реализуем метод cellForRowAtIndexPath ( Swift 2 ):
Получаем список фотографий с сервера Flickr.com:
Все очень здорово и просто в Swift 2 , но в Swift 3 запрос NSFetchRequest<NSFetchRequestResult> стал Generic , а следовательно, стал Generic и класс NSFetchResultsController<NSFetchRequestResult> .
Переменная 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 , то также удобно использовать миграционный «робот». Надеюсь, он вас не подведет.