Уже известно, что 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
.
Мы предпочтем первый вариант и будем использовать пустую строку " ", если выражение равно 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
так располагается в порядке убывания:
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
.
В классе, наследующим от CoreDataTableViewController
, cоздаем NSFetchResultsController
с помощью инициализатора, включающего в качестве аргумента запрос request
, а затем присваиваем его переменной var
с именем fetchedResultsController
. Как только вы это сделаете, таблица cо списком фотографий начнет автоматически обновляться (Swift 2
):
Конечно, реализуем метод 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
, то также удобно использовать миграционный «робот». Надеюсь, он вас не подведет.
Ссылки: Yammer iOS App ported to Swift 3
Автор: WildGreyPlus