Сегодня я хочу вам рассказать о том, как однажды познакомился с языком программирования Swift и решил написать на нем приложение для социальной сети ВКонтакте под OSX (которое, к сожалению, до сих пор не закончено). С какими подводными камнями мне пришлось столкнуться при обуздании, на тот момент, нового языка и скрещивании его с VK.API. Поделиться с общественностью результатом того, во что именно все это вылилось и попытаться обосновать, зачем нужно было придумывать очередной велосипед в виде библиотеки для работы с VK.API.
Если кому-то это интересно, то добро пожаловать под кат.
Часть первая: Hello, Swift!
Это был вечер 2го июня 2014 года. Являясь пользователем продукции Apple и начинающим objc разработчиком, я был прикован к экрану своего ноутбука, на котором мелькали друг за другом кадры презентации WWDC 2014. Проводить один вечер в году за просмотром трансляции уже стало стандартным обрядом. Этот раз не оказался исключением. Как и ожидалось, ничего сверхъестественного представлено не было, а новый интерфейс OSX даже оставил некий налет разочарования. Я уже всецело намеревался досмотреть оставшиеся 15 минут и отправиться по своим мирским делишкам. Но тут, под шумок очередного обновления Xcode, без знаменитого «one more thing», бесцеремонно, Крейг Федериги представляет новый язык программирования, имя которому Swift.
Это было именно то, чего я так ждал. Сколько я не пытался привыкнуть к синтаксису objc, но многие моменты доставляли большие неудобства. Это было похоже на неустранимый зуд, который переодически напоминал о себе в процессе написания кода. Я помню, что за пару недель до этого самого дня, мечтал о том, чтобы objc был человечнее и читабельнее. Даже набрел на статью на хабре по этому поводу, но счел решение костылем, который только будет больше мешать, чем приносить пользы. И вот, оно совершилось. Я не мог поверить. но оставшийся вечер точно был потерян. Все планы отодвинулись на второй план. На первый же выдвинулась задача скорее стянуть новый Xcode и узнать, что же из себя представляет эта диковинная птица. Я игрался с новинкой вплоть до того момента пока царство снов не поманило меня к себе, а сил сопротивляться больше уже не осталось. Но запасшись книжкой «The Swift Programming Language» зарекся, что с самого утра подойду вплотную к изучению всех тонкостей нововыпущенного на свет стрижа.
Сказано — сделано. Я с воодушевлением взялся за поставленное самому себе задание. Перечитал всю вышеупомянутую книгу и очень въедливо старался разобрать примеры кода из нее, дабы понять что же тут к чему. А когда с ней было покончено, успешно владел основными конструкциями и влюбился в этот язык. Упрощенный читабельный синтаксис, статическая типизация, опциональные значения, дженерики и многие другие вещи Swift не оставили меня равнодушным. Возвращаться обратно к старичку objc больше абсолютно не хотелось(хотя еще, наверное, долго придется).
Часть вторая: Вперед и с песней
Итак. Учебные материалы по языку были выучены, основы разобраны, а углубляться дальше в тонкости хотелось настолько, что было принято слегка опрометчивое решение писать приложение на языке, который еще находился в статусе бета.
Стоит сказать, что до этого я писал лишь небольшие программки для автоматизации рутинных действий. Не сказать, что это были прям какие-то скрипты. Нет, это были маленькие приложения на objc, с применением ООП, многопоточности, графическим интрефейсом и даже каких-то шаблонов проектирования, которые до сих пор успешно работают и повседневно упрощают жизнь. Только вот они решают специфические для меня задачи, и врядли по этой причине кому-то сильно пригодятся (хотя есть идея довести их до соответствующего состояния и выложить в стор. Вдруг я ошибаюсь).
Мотивацией для всех моих детищ была одна мысль — если этой вещи нет или реализовано не так, как хотелось бы, а я знаю, как хотел бы это видеть, то нужно делать самому. В этот раз я последовал той же логике. Уже какое-то время была в голове идея создать приложение для социальной сети вконтакте под OSX, так как аналоги меня никак не устраивали (не буду говорить, что за приложение, так как оно еще далеко от релиза. Да и суть статьи не в этом). Проект предстоял намного крупнее того, что я обычно делал и требовал больших трудо и мозгозатрат, чем обычно. Я подумал, что это хорошая возможность углубиться в разработку на Swift, в так же воплотить в жизнь отложенную идею. Тем более один мой друг уже был знаком с работой VK.API и тоже хотел изучить Swift. Это еще больше воодушевило меня.
И вот, я начал пытаться построить у себя в голове более четкую концепцию того, что хотелось бы видеть по прошествию работы. Только один, так как друг мой этой идеей, видимо, загорелся меньше. Но с API он мне все же помог, за что ему отдельное спасибо.
Первое, что пришло в голову — найти библиотеку для работы с VK.API для Objective-C (благо Swift позволяет использовать код на objc в своих проектах). Это должно было сильно упростить разработку, не пришлось бы заморачиваться с логикой запросов/ответов, а только получать данные, раскидывать их в модели и так же просто отправлять новые данные на сервера.
Копавшись в официальной документации VK я первым делом обратил внимание на официальный SDK. Создан он для iOS, но может можно как-то прикрутить к OSX? Оказалось, что нет. Помимо самих запросов там очень многое завязано на UIKit, а перекручивать гайки под AppKit было почти нереально. Отчаянный поиск в гугле тоже ничего не дал. Все библиотеки были разработаны либо исключительно под iOS, либо являлись уже как несколько лет назад закинутыми автором на пыльную полку недоделками. Ни один вариант мне не подошел. Это, конечно, меня опечалило, но раз уж взялся за воплощение затеи, то останавливаться не стал. Нет библиотеки? Обойдемся без нее!
Часть третья: Мы свой, мы новый дом построим
Первое, что нужно было сделать — разобраться с процессом авторизации. Тут все оказалось просто потому, что он проходит по протоколу oAuth. По крайней мере авторизоваться и выдрать токен с определенными правами из адресной строки браузера, чтобы начать пытаться отправлять тестовые запросы к API получилось быстро. Потом, что немного сложнее, нужно было реализовать это в самом приложении. Был создан WebView, который загружал страницу авторизации а его WebFrameLoadDelegate смотрел на URL страницы и ловил токен. На первых порах было всего три варианта действий: Если в URL есть токен, то забираем его и убираем WebView. Если пользователь отклонил авторизацию, то вообще закрываем приложение, a если вдруг решил перейти на какую-то другую страницу, то не даем ему это сделать и возвращаем обратно к авторизации.
Запросы я отправлял с помощью NSURLConnection, причем синхронно. Ответы парсил с помощью NSJSONSerialization и в итоге получал вот такие конструкции:
(((jsonObject as? NSDictionary)?["phoneNumbers"] as? NSArray)?[0] as? NSDictionary)?["number"] as? NSString
Согласитесь, не самое приятное зрелище. В этом месте работа с опциональными превращается в ад. Подробнее о проблеме здесь.
Где-то здесь начало становиться понятно, что все происходящее не есть хорошо и удобно. Плюс от API могут вернуться ошибки, запросы на авторизацию, аутентификацию, ввод каптчи. Эти вещи нужно как-то предусмотреть и обработать. После недолгих размышлений было решено, что максимально логично абстрагировать всю логику работы с API в отдельную библиотеку. Во-первых это позволит не размазывать код общения с VK по приложению, а во-вторых его можно будет переиспользовать и устранять ошибки более локально. Так как библиотек для iOS и objc есть куча (хотя хватит и одного официального SDK), то я решил делать свою исключительно для Swift и OSX.
Вообще, стоит сказать что если бы я писал на iOS, то такой проблемы бы и не возникло, но так как я десктопный, точнее ноутбучный, пользователь и люблю OSX больше, чем iOS, то все вещи которые делаю для себя — делаю естественно под ту платформу, которой активнее пользуюсь. Да и возможностей у которой, по моему скромному мнению, больше. Но на вкус и цвет…
Часть четвертая: Разработка. Или беглый обзор SwiftyVK
Первой появилась структура под лаконичным названием VK — точка входа, авторизации, создания запросов и просто дирижер SwiftyVK. Сначала она имела только методы для авторизации и деавторизации + набросок протокола делегата, который нужно реализовать в классе использующего приложения.
Дальше нужно было реализовать полноценную авторизацию. За основу было взято уже имеющееся решение и доведено до ума. Приложение при авторизации должно передать права, которые оно хочет получить. В официальном SDK список прав передается, как массив строк. Я решил воспользоваться статической типизацией Swift и сделал перечисление(enum) из прав, что позволяет при авторизации передавать массив так:
[.messages,.offline,.friends,.wall,.photos,.audio,.video,.docs]
Раньше токен, полученный после авторизации, хранился как переменная до закрытия программы. Теперь точно нужно было позаботится о его хранении получше. Если сильно не вдаваться в подробности, то это объект класса Token, который хранит непосредственно строку с токеном, логику ее отдачи и проверки актуальности. Если токен устарел, то будет запрошен новый. Token поддерживает протокол NSCoding и может сохраняться в NSUserDefaults или любое другое место на выбор. Сам объект существует как синглтон, так как два токена нам, естественно, не нужно.
Авторизацию можно запустить как в любом окне в ввиде sheet, так и отдельным окном. После прохождения авторизации мы начинаем отправлять запросы API. Нужно было реализовать логику их отправки и получения ответа. Это сделано группой классов/структур Request, Connection, Response и NSURLFabric. Вне самой библиотеки видна только первая — Request и некоторые ее свойства/методы, которые позволяют настроить запрос и отправить его в дальние дали к серверам «ВКонтакте». Connection — логика отправки запроса, ожидания ответа, обработка ошибок и вызова калбэков. NSURLFabric — фабрика со статическими методами, которая собирает из Request настроенный и готовый к отправке NSURLRequest. Ну а Response — простая структура с полями request, error, success и логикой парсинга ответа от сервера в ошибку или успешный ответ.
Выше было показано как парсился JSON с помощью NSJSONSerialization. Чтобы получить более удобные и Swift-нативные методы получения значений сначала была использована библиотека из приведенной там же статьи с описанием этой проблемы, но так как она все равно оставляла много мест, где используется знаки вопросов при парсинге, позже совершен переход на SwiftyJSON, как по мне, очень удобную библиотеку. С ее помощью проблема была решена полностью.
Ошибки реализуются в классе Error, который напоминает NSError. Объекты Error даже могут быть инициализированы с помощью NSError, но чаще это происходит из JSON. Обработка некоторых ошибок происходит прямо в SwiftyVK. Напрмер, если приложение отправило запрос который требует авторизации, но не мы не авторизованы, то вернется сообщение об ошибке, покажется окно авторизации, а после ее прохождения запрос будет переотправлен. Аналогично с каптчей. Чтобы отключить данную фичу нужно поставить параметр запроса catchErrors в false. Тогда даже в случае возвращения известной SwiftyVK ошибки будет выполнен блок обработки ошибки.
Долго же я мучился с синхронными и асинхронными запросами. Не в плане сложности многопоточности и асинхронности. Здесь ничего сложного. Проблемы были из-за отправки синхронных запросов в главном потоке. Могла получиться такая ситуация — приложение шлет синхронный запрос. Поток, из которого он отправлен, блокируется до момента вызова блока ответа или ошибки. API возвращает ошибку, которая говорит о необходимости авторизации/ввода каптчи до вызова блока ошибки. Окно нужно показать из main thread, а он уже заблокирован в ожидании ответа. В итоге взаимоблокировка и висим наглухо. Сколько я не пытался придумывать костыльные способы обхода этого — где-то что-то шло не так. Выход — на синхронные запросы с проверкой ошибок из главного потока сработает assert с описанием, что не стоит их слать. Конечно, не лучший способ, но всяко лучше, чем глухое зависание.
Так как Swift строго типизированный язык, то хотелось все строго типизировать, чтобы мест для совершения ошибок в будущем было как можно меньше. Самое, возможно, для кого-то спорное место — типизировать аргументы запросов. В официальном SDK они передаются в виде строки. Я же решил ввести пересечение Arg: String, в котором перечислил все на данный момент возможные аргументы запросов. По мимо минимизации ошибок, это позволяет пользоваться автодополнением в Xcode при задании аргументов.
Для еще большего удобства введена структура API, в котрой по группам-подструктурам содержатся методы для быстрого создания запроса к определенному методу API. В итоге создание запроса происходит как-то так:
let req = VK.API.Friends.get([
.count : "1",
.fields : "city,domain"
])
Как по мне, очень просто, читабельно и лаконично.
Помимо отправки простых запросов, есть возможность отправлять запросы с кастомным именем метода(передав его строкой), вызова execute или удаленных процедур. Плюс реализация загрузки файлов(чего в большинстве других библиотек нет) и LongPool клиента (этого нет нигде).
Часть пятая. Последняя
Впринципе, это все, из чего состоит SwiftyVK. Долгое время я его не выкладывал, так как тестировал и доводил до ума. Думаю, сейчас он готов к встрече с общественностью. Я старался сделать максимально простую и понятную библиотеку для работы с VK.API с использованием преимуществ нового языка. Хочется сказать, что в конце разработки я отступился от идеи делать ее только под OSX и добавил равноправную поддержку iOS. Хотелось еще сделать для tvOS, но там пока нет WebView и невозможно авторизоваться с помощью oAuth. Как только будет возможность — так сразу. Под watchOS решил не делать, ибо не знаю кому это может понадобиться. Я даже не уверен, что эта библиотека вообще может кому-то понадобиться, кроме меня. Но если я ошибаюсь и кто-то заинтересовался SwiftyVK, то боьлшое Вам спасибо и удачи в использовании. О всех пожеланиях обязательно пишите — будем делать.
Ознакомиться с документацией на кривом английском и исходным кодом можно здесь.
P.S.: Если Вы дочитали до этого места, то большое вам спасибо за уделенное время.
Комментарии (7)
storoj
29.10.2015 19:12+1Сам объект существует как синглтон, так как два токена нам, естественно, не нужно.
естественно ли?WEStor
29.10.2015 21:26-1Если Вы хотите авторизовать сразу нескольких пользователей, то, конечно, правда Ваша. Но архитектура данного фреймворка разрабатывалась без этой возможности. Может быть, позже подход будет пересмотрен, если будет заинтересованность в данном функционале.
storoj
29.10.2015 21:40+1всё равно это чудовищное решение
ну и пример:
есть какая-то пользовательская сессия, в ней очередь запросов, происходят какие-то события. и тут пользователь разлогинился. гораздо безопаснее просто оставить эту сессию на произвол судьбы и создать новую, чем контролировать корректное завершение всех отложенных запросов, потому что черт его знает кто где и когда что запросил. а так она когда-нибудь доделает все свои запросы и спокойно себе сдохнет
InstaRobot
30.10.2015 00:05Сказать честно, я не сторонник технологий, которые еще достаточно долго будут «допиливаться» и ломать код, для разработки использую старый добрый Objective-C. Но статья заинтересовала несколькими моментами:
1) Автор не поленился с разработкой под десктоп, что само по себе геройство, в наше время «готовых кирпичиков». Под десктоп, если хотите кодить, то приходится долго и муторно читать документацию Apple. Мои Вам аплодисменты.
2) Я сам разрабатывал приложения для ВК, потому тема очень близка и в свое время пришлось отказываться от части методов, реализованных в стандартном СДК.
Очень надеюсь, что проект будет развиваться и далее!WEStor
01.11.2015 14:33Да, свифт поначалу доставлял много приключений с обновлениями, но сейчас ситуация уже лучше. Хотя вот чего не хватает с свифте до сих пор — это такой же дебаггинг, как в objc. LLDB пока не дает таких же удобств для стрижа.
С разработкой под десктоп и вк вы верно подметили. Именно это и толкнуло на то, чтоб сделать библиотеку, которой можно поделиться. Хотел еще в статье написать, что на том же stackOwerflow по iOS активности в разы больше, чем OSX. Иногда найти ответ на вопрос возможно только в доках или эмпирическим путем. Но мы, видимо, сложностей не боимся.
Большое Вам спасибо за понимание и пожелания. После выхода статьи видно, что заинтересованность библиотекой есть. Будем ее потихоньку допиливать)
Streetmage
«Save token to NSUserDefaults» — не делайте так никогда, название репозитория мб SwiftyVK i.gyazo.com/bbb95ac1ea2e389042ee80bd69aa0627.png
WEStor
Спасибо, поправил. Видимо где-то нужно было опечататься. По поводу сохранения токена — это только возможность для тех, кому лень свое расположение задать)