От прототипа до прототипа, от прототипа до прототипа, от прототипа до… мусорки

Захотел разработать небольшое приложение — Qtty. Приложение должно уметь делать снимок и применять набор фильтров к нему, после чего публиковать этот самый снимок в качестве основной фотографии в профиле ВКонтакте.Автор будет пробовать делать всё через прототипы, так как это делали в 223 сессии WWDC 2014.Описание проектаИзначально описание выглядело таким образом: Приложение будет работать только с ВК (пока) и позволять пользователю делать фотографии и сразу их загружать в нужный альбом в ВК, по возможности применив какие-то фильтры.

Основной функционал:1. Отображение списка фото альбомов2. Отображение фотографий выбранного альбома3. Возможность удалить фотографию4. Возможность удалить альбом5. Возможность создать альбом и указать права доступа к нему6. Сделать фотографию, применить фильтры, указать альбом в который загрузить фотографию (либо сделать основной картинкой профиля), прикрепить местоположение, добавить описание к фотографии.

Решил, что хочу сделать что-то по-быстрому, поэтому убрал большую часть функционала и сделал такое приложение, которое описывается первым абзацем:

Приложение будет работать только с ВК (пока) и позволять пользователю делать фотографии и сразу их загружать в ВК, по возможности применив какие-то фильтры.

Прототипы. Раз. Делаем любые зарисовки экранов, предлагаем самые разные идеи и любым образом наносим их на бумагу.Начал я с экрана авторизации. Без авторизации приложению нет смысла обрабатывать фотографию, поэтому исключен вариант показа экрана для осуществления снимка в качестве стартового. На экране авторизации должен был находится какой-то элемент вроде кнопки, после нажатия на который пользователь бы смог авторизоваться в ВК и мы смогли осуществлять запросы от его лица.Какие наброски были у меня: feaa382c5898e337d10a444317030122.jpgfb882e86b4053771ed67ca3c69abf5cc.jpg

Объясню каждый их них.1. Задумка была в том, что перед пользователем будет некий набор фотографий на которых будут надписи (Делись впечатлениями с друзьями, Будь всегда на связи, Делись радостными моментами и тд). Ниже находится кнопка авторизации.Минусы этого варианта: Надо что-то листать.Лишняя работа по подбору картинок нужных тонов, которые будут сочетаться с фоновым цветом + подбор шрифта и его цвета для нанесения на изображения.Смахивает на какое-то руководство пользователя.Мне он просто не понравился.

2. Если в первом варианте не было видно, какое изображение ты листаешь по счету и сколько из всего, то здесь этот недочет исправлен. Однако минусы остаются такие же.3. Картинка и кнопка. Казалось бы всё просто, но возникает масса вопросов: цвет фона, цвета картинки, их сочетания. Вариант отпал.4. Здесь фоном установлена картинка с множеством пользовательских фотографий, на картинке может быть некий дополнительный слой, чтобы множество лиц не резало глаза и у пользователя не вызывало ощущения тревоги и нервозности. Кнопка остаётся.Этот вариант мне больше всего понравился.

После того, как все наши идеи были перенесены из головы на бумаги взялся делать наброски этих самых экранов в реальных размерах с реальными элементами. Всё делалось в Keynote.Покажу не все варианты, а только 2, которые я смог сделать. В ходе реализации первых двух вариантов, с перелистываниями, пришел к выводу, что слишком громоздко для такого маленького приложения.

Самый первый предварительный вид экрана авторизации: 8a35b2fca92a62e1d89163aee94cb117.png58fc0661c888f3be862999398e302466.png

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

Спустя некоторое время появились еще такие варианты: 3cc8735a9f0f5af25c3411b784671907.pngc143bdca7faba60b77e97a412bc629f9.png

Под рукой не было бумаги, поэтому экран отображения ВК в UIWebView для авторизации был набросан сразу в Keynote. Варианты, которые получились: 45e8017ab0bb3cc70ae2d879455008be.png9b6caef52cc57f518778bbd6328fa4c0.png3bd6ad8ab1d33b15af729e6cbc4db081.png

0842708ebea017516630467f3365dac1.pngedea980e6882b59e777f9e45a5ba402c.png4db11f8459a47deb791102408a1476e5.png

Остановился на первом варианте. С кнопкой «Закрыть»/«Отмена» верхняя панель выглядела не так, как хотелось бы. Пробовал добавить туда кнопку с разными тенями/прозрачностью и символом «Х», но тоже выбивалось (хотя начинает нравится идея со стикерами или элементами-наклейками).96cc8af037c11a995b777bbdea9d819e.png575013b61da3b5bdb9d434c1e356e83d.png

Хочу еще раз напомнить, что основная задача на этапе прототипирования — воссоздать визуально приложение не программируя, испробовать множество видов и вариантов экранов, посмотреть на реакцию пользователя и услышать их ответную реакцию (уже на ранних этапах прототипирования мы избавляемся от каких-то проблем, которые бы обычно возникли где-то в середине разработки приложения или в конце).

Следующим экраном будет экран фотографии, где пользователь сможет сделать снимок (используя фронтальную или заднюю камеру) и перейти на экран применения фильтров/эффектов.С экраном фотографии было куда интереснее. Просмотрел и поработал с Инстаграмом, кое-какие моменты мне не понравились и я решил устранить их, но в то же время перенять кое-что себе.Что получилось (от начальных вариантов до последних): 1840e9a6ef0c6f80d9749bddf41edcab.jpg20b93e1e9af69ae4776e6ccb5d856789.jpg23180b24ec03ea692f5e02e0a7fd7f3b.jpg

Данный экран не хотелось загромождать чем-то кроме самых основных элементов, поэтому в целом можно сказать, что все варианты это некие вариации одного. Изначально в нижней части экрана находится только одна кнопка (один элемент) по нажатию на который пользовательские снимок будет зафиксирован. После фиксации снимка из кнопки (в моём представлении) должны «выехать» в противоположные стороны две дополнительные кнопки — «Переснять» и «Обработать». Как видно на одном из набросков, на начальном этапе кнопки были только с надписями «YES» и «NO», но второй вариант (первый снимок, второй экран) натолкнул меня на мысль, что YES/NO нисколько не информативно, но в тоже время отображать вопрос «Что хотите делать со снимком?» излишне, потому что из контекста понятно, какие действия могут быть и какие нужны.Почему я решил опустить кнопку с буквой «Q» в нижнюю часть экрана таким образом, что она стала немного заезжать вниз? Ответ прост — желание увеличить свободное пространство. По этой самой причине на 3 снимке (1 экран) верхняя панель была удалена.Из Инстаграма я перенял только сетку, мне она показалась необходимой в случае, если я хочу сделать пару снимков таком образом, чтобы объект находился в центре фотографии.

Вот, что я получил спустя 40–50 минут (см ниже). Кое-какие моменты были изменены, кое-какие еще будут изменены, есть идеи. Благодаря прототипу удалось заметить, что просто наложить знак решетки и фотоаппарата на изображение не получится — надо что-то подкладывать под них. Панель — первое решение, но из-за неё теряется целостность и впечатление от фотографии, она мешает.Видео процесса прототипирования можете найти по этой ссылке.

92ebccf3e417bd9985b0cc647641e58c.pngc1be3910c35e2f73a7ffb826f377ca9c.png4517670751bb43abc163c01003375fd4.png

f52a3acfb900feedce78566bbdd4b027.pngf107705e452c9af8ec22df17a4ef62df.png05b866534b21928ec9be03a3fcd18ce7.png

Пока переносил элементы с портрет-режима в лэндскейп-режим задумался о том, чтобы убрать статус бар.Сравните: 394f0d234f3c38587d2417877f6565b3.png5a0cb2453b78681f66d0d3d2f105e9d3.png2ad813a87c4be7a91327965bd3dd3657.png

Сравните: b1ac8d2d055b44249e4bb83cd3c05103.png44ed78adea7ab41d5a9a4062e8a95173.png1325874e950972158eb0e3bf3efe943e.png

Сравните: 5c37f8286a41ee298a76a56dc164c1a8.png54d1015619fe568daab6ae06c72df37c.pngefe73094b2bf83186f6d156a1307e467.png

Может попробовать сделать так? 24b9b4ca5ce66e14ceba2130bde01d7a.png37f312621de5b64349bb0dadccb3e73f.png

А в портрет режиме перенесём элементы вниз: 5eeb10fe8fa0fed70e3ae2f3409bd74d.pngef936c306b6181c6f3e3e477621cb3ea.png4f52ff0c4984566c91938bddaa6d5244.png

Спустя минут 20–30 решил остановиться на таком варианте (чем больше сейчас вариантов, тем шире потом будет выбор): f4d12f88d6a4eba9f7d7fa2ccda248fb.pnga822151f0f4140af0363ea25fa888abc.png19e3bde01b8bb479370e081d038949e3.png

74afd4df481395ac0454663ed2ce7727.png97b3b5e08ab4c8e9dbae46c12725e760.png

Фиолетовая кнопка мешает. Фиксирование снимка будет по нажатию, после нажатия перед пользователем не будет появляться никакого запроса дальнейшего действия — сразу отображение фильтров, кропов и т.д. С этого экрана пользователь сможет одним нажатием перейти к съемке.

Приступил к наброскам экрана обработки фотографии. Первые версии выглядели следующим образом: e2f13e65e9a7e5524bc00afa42ad17bf.png4ec8eb3aeb4026fd1b1fed524e3cc5b5.png

Вариант понравился, но я бы увяз в реализации, чего мне не хочется.

Упростил до такого: 1c8ceca4d6bf2b0aee7d2e44276322df.png20713f7297859e8714a93f4610b508a8.png6c4625cc975d336fe05d733e64af8929.png

В этом варианте решил обыграть тему со стикерами/наклейками. Как можно заметить, в первой версии этой темы — плоские элементы.

Такими получились у меня экраны обработки фотографии в лэндскейп режиме (выбирать потом будем, но уже есть несколько фаворитов. Видео по этой ссылке.): 618d5d86fba9b802187e6c19f4d7dfff.png37718c336b3a343dbd61e04823933ace.png4687f8fd8b7bebb45f0cee1a6013b3f3.pngf883448f007fb9cd48f60352999cc9ed.png1265e80a2d78e04397e7dec86bcae50f.png

Кое-что совершенно другое (каждое изображение должно быть исходным изображением с наложеным фильтром): 1041227da7a24401d85db107f41ce814.png

После нажатия открывается такой экран: a83e51f0fd333695bc70868d9b292f63.png

Правда у такого варианта несколько вопросов:1. Как уйти с этого экрана?2. Два элемента, которые подтверждают действие какое-то (кнопка — применяет фильтр, а галочка — переход к настройкам)3. При небольшом кол-ве фильтров выглядит хорошо и достаточно большая часть изображения видна, но как будет выглядеть эта гармошка при 10–15 фильтрах? Всё-таки смотрится приятно, поэтому окончательно не будем отбрасывать этот вариант.

Следующим у нас идёт экран указания опций загрузки. Что пользователь может указать:1) Установить фотографию в качестве основной2) Прикрепить текущее местоположение3) Выбрать альбом в который будет загружена фотография4) Добавить метку друга на фотографию5) Запостить фотографию на стену

Наброски в Keynote: 37bc798fd22670ae8845255ae92087bc.pngfd0785102b3cbb2c0cbffe0e97db3936.png476fc2ce5383fac16a706c92ad571473.png

56469bb57c58693a5e3fec4f2bce9652.png71084de9d39df8b47232e7866adacfef.png233d8a3453c5d9395b15313c9002d859.png

37fed6042dfb20fa53fba156dd9d5041.pngd256c267d164a246cd820ccc10698404.png

Видео процесса создания можно найти по этой ссылке.

Стоит ли показывать пользователю процесс загрузки фотографии? Если ответ да, то необходимо разместить некий индикатор загрузки на текущем экране, либо создать новый экран со списком фотографий, которые должны быть загружены. Список нужен для того, чтобы решить проблему загрузки фотографий при отсутствующем интернете. Насколько нужен пользователю такой список в явном виде? Основная задача — спрятать всё лишнее и сделать работу приложения максимально органичной и естественной.После нескольких минут прикидок пришел вот к такому варианту отображения кол-ва фотографий в очереди: 0989a2bea485ae824df264ffed30f981.png6afccd5e72e5dc86bf099e5c5521f408.pnge079c076f667a2a4bcadac74d90f60a1.png

Загружать изображения будем в фоновом режиме. Если по какой-то причине загрузить фотографию не удалось, то повторим попытку позже, но не будем удалять фотографию из списка.

Последней экран с опциями мне не особо нравится, решил пошаманить еще. Сравните: 1a5ecb801cfe6f7fdb11d0890c5195e8.png039de68122853ebcba63fe237bc8c746.png

Такие получаются интерфейсы в лэндскейп режиме: b4ca735ef561cb1869e63f01a1b9e0c4.png49867a6b513d031dd03d90adb549cc68.png

Вариант с callout-блоком даже не стал рассматривать. Места вертикального в лэндскеп режиме мало + хотелось сохранить некоторые общие положения элементов между лэндскейп вариантом и портретным.

Прототипы: отзывы. Два. Экран авторизации: 4063525012ba93846a7d44a3f6e85ebe.pngКомментарии: отсутствуют.Модальное окно авторизации в ВК (корректированная версия): e6596dbfd24ab4160e4d3b8e06af6903.pngКомментарии:

Надпись «Авторизация» на прошлом скрине смахивала на кнопку и у испытуемых возникали вопросы по этому поводу.

Окно съемки (портретный режим): 33dc331a44f337c80fd58ab81dd1bdd0.pngКомментарии: отсутствуют.

Окно съемки (лэндскейп режим): d1cc04c424251e92ad80088acf5090ad.pngКомментарии: отсутствуют.

Фильтры (портретный режим): bc86f65616cac5485fc5685eeb393b42.pngКомментарии: отсутствуют.

Фильтры (лэндскеп режим): c8721be7063f9977cda663f178381b01.pngКомментарии: отсутствуют.

Настройки загрузки (портретный режим): 8b5f24b8b5180701ea0a6a6e95386d4b.pngКомментарии:

Шрифт отстой. Зачем нужен заголовок «Друзья», если я итак вижу, что это мои друзья? (автор: так появился второй вариант экрана)

Настройки загрузки (лэндскейп режим): bc416e1abf0c9aa8e844cb3056e57084.pngКомментарии: такие же, как к экрану в портретном режиме.

Прототипы: анимация. Три. Видео можно просмотреть по этой ссылке.Программируем главный экран. На этом экране я решил провести несколько экспериментов:1. Использовать блюр эффект в качестве «стеклянной» прослойки между фоновой картинкой и названием приложения.2. Кнопку авторизации вставлять не картинкой с тенью, а программно создавать её.Начнем с того, что использование блюра в чистом виде не даст нужного эффекта.

Надо хитрить — добавлять другой слой. Так и сделаем. Дополнительный слой будет серого цвета и для достижения нужного эффекта (как на макете) необходимо экспериментировать с прозрачностью самого слоя (alpha).

Логотип приложения вставлялся простой картинкой.

С тенью было разбираться куда интереснее, дело в том, что нельзя на одной вьюхе применить cornerRadius и кастомный shadowPath. После гугления и чтения документации решаем вынести тень на отдельный слой. Для достижения нужного стикер-эффекта, необходимо определить границы тени под кнопкой, для этого воспользуемся GGPath и вспомним геометрию.

Если внимательно присмотреться к тени под кнопкой, то можно прикинуть, что она состоит из «почти»-прямоугольника с одной округленной стороной (нижней). С построением прямых никаких проблем не должно возникнуть — CGPathAddLineToPoint () и CGPathMoveToPoint (). Что же касается округленной стороны, то у нас выбор невелик — либо мы хитрим и округленную сторону заменяем двумя прямыми, либо разбираемся с методом CGPathAddArc () и подобными.Скажу сразу, что используя первый способ не получится добиться желаемого эффекта.

На какие вопросы необходимо ответить?1. Как определить угол «упирающийся» на хорду зная длину хорды и радиус?2. Что же такое радианы и как выглядит окружность от 0 до 2Pi?

С первым особых трудностей возникнуть не должно. Есть формула для вычисления длины хорды, в ней есть переменные радиуса и угла, необходимо выразить угол и подставить нужные значения.Во втором вопросе, при реализации, я столкнулся с тем, чего не ожидал увидеть.Вот, как выглядит окружность с отмеченными углами в радианах: ddbb3e2c0a57238db3c432162d95cb90.png

Отсчет идёт против часовой стрелки, в реализации же Apple отсчет идёт по часовой! Где 270 у нас, 90 — у них.Я такого не ожидал, может что-то упускаю?

Код отрисовки тени выглядит следующим образом (ЗЫ:, а подсветки для Swift нет):

// создаем тень let shadow = UIView (frame: CGRectMake (0, 0, CGRectGetWidth (frame), CGRectGetHeight (frame))) shadow.layer.shadowOpacity = 0.65 shadow.layer.shadowRadius = 4 shadow.layer.shadowColor = UIColor.blackColor ().CGColor shadow.layer.shadowOffset = CGSize (width: 0, height: 0) // создаем форму для тени let diameter = (CGRectGetWidth (frame) / CGRectGetHeight (frame)) * CGRectGetWidth (frame) let radius = diameter / 2 let xCenter = CGRectGetWidth (frame) / 2 let yCenter = CGRectGetHeight (frame) + radius let angle = 2 * asin (CGRectGetWidth (frame) / (2 * radius)) let endAngle = M_PI + (M_PI — angle) / 2 let startAngle = endAngle + angle let shadowPath = CGPathCreateMutable () CGPathMoveToPoint (shadowPath, nil, 0, CGRectGetHeight (frame) / 2) CGPathAddLineToPoint (shadowPath, nil, frame.size.width, CGPathGetCurrentPoint (shadowPath).y) CGPathAddLineToPoint (shadowPath, nil, CGPathGetCurrentPoint (shadowPath).x, frame.size.height) CGPathAddArc (shadowPath, nil, xCenter, yCenter, radius, startAngle, endAngle, true) CGPathAddLineToPoint (shadowPath, nil, 0, frame.size.height) CGPathCloseSubpath (shadowPath) shadow.layer.shadowPath = shadowPath Можете сравнить (слева макет, справа реализация): 1a2d75239ecda9fce559e50cea246570.png98a971d377e9c7408f751c4513eb45aa.png

Программируем экран авторизации в ВК. Первое, что необходимо сделать — интегрировать Vkontakte iOS SDK в проект. Vkontakte iOS SDK написан на Objective-C, есть вот такой туториал по тому, как интегрировать Objective-C в Swift проект.Подключить SDK никаких проблем не составило.Запуск процесса авторизации начинается с вызова такого метода:

VKConnector.sharedInstance ().startWithAppID (»87687678678», permissons: [«photo», «wall», «friends»], webView: self.webView, delegate: self) ce54126b5c452456b92a1ca35a011426.pngПосле успешной авторизации получим токен доступа, который будет сохранён в хранилище данных SDK. Пользователя перекинет на экран фотографии.

Программируем экран реализации фотографии Начну с того, что покажу итоговые результаты: 605ef6aed943cda4b4f971288c32558b.png73a858ba1aef0b898991fdda29380192.png72883cc743a7f176687c378fc8eb8eb8.pngСетка убирается и сделана такой незаметной специально.

Вот такой вариант расположения кнопок был тоже, но его отбросил, потому что при нажатии на границе экрана (кнопка — экран) рядом с кнопкой частенько делал фотографию, а не переключался между камерами или активировал сетку: 4fea8bb5aae2b138defda42a3d868767.png8ae4ef8befdf6d4fe58da216f24c7948.png

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

За основу я взял UIImagePickerController, который подходит для решения поставленной задачи достаточно хорошо. Скрыл все его элементы, убрал всё лишнее, установил свою cameraOverlayView.

let cameraView = NAGImagePickerController () cameraView.delegate = cameraView cameraView.sourceType = UIImagePickerControllerSourceType.Camera cameraView.showsCameraControls = false cameraView.allowsEditing = false cameraView.cameraOverlayView = NAGFirstPhotoOverlayView (frame: UIScreen.mainScreen ().bounds) Небольшим сюрпризом для меня оказалось появление вот этого визуального элемента зума: 7dfe9034726a98fa43803b67223cf312.pngОн мне здесь не нужен был, да и портил очень вид, к тому же перекрывал кнопку — надо убирать. Реализовал первый способ, который пришел в голову:

let pinchGR = UIPinchGestureRecognizer (target: self, action: nil) addGestureRecognizer (pinchGR) Потом решил поискать на SO какие-то более интересные варианты решения и нашел — установка userInteraction в false.

Рассмотрим теперь самую интересную строку, на мой взгляд с неё начинается самое интересное:

cameraView.delegate = cameraView Два вопроса:1. Так как при появлении UIImagePickerController я должен запустить анимацию появления кнопок, то каким-то образом надо получать событие/уведомление об этом, а значит мы должны перезаписать метод viewDidAppear самого UIImagePickerController’а.2. Необходимо неким образом уведомлять UIImagePickerController, что надо сделать фотографию или переключиться на фронтальную камеру

Нашел два решения:1. Вынести в глобальную область видимости UIImagePickerController2. Создать подкласс UIImagePickerController и реализовать его таким образом, чтобы он отправлял уведомления о событиях, которые произошли «в нём» и мог обрабатывать команды сам (сделать фотографию, изменить камеру и т.д) — NSNotificationCenter

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

// // NAGImagePickerController.swift // Qtty // // Created by AndrewShmig on 21/07/14. // Copyright © 2014 Non Atomic Games Inc. All rights reserved. //

import UIKit

let kNAGImagePickerControllerViewDidLoadNotification = «NAGImagePickerControllerViewDidLoadNotification» let kNAGImagePickerControllerViewWillAppearNotification = «NAGImagePickerControllerViewWillAppearNotification» let kNAGImagePickerControllerViewDidAppearNotification = «NAGImagePickerControllerViewDidAppearNotification» let kNAGImagePickerControllerViewDidDisappearNotification = «NAGImagePickerControllerViewDidDisappearNotification» let kNAGImagePickerControllerViewWillDisappearNotification = «NAGImagePickerControllerViewWillDisappearNotification» let kNAGImagePickerControllerFlipCameraNotification = «NAGImagePickerControllerFlipCameraNotification» let kNAGImagePickerControllerCaptureImageNotification = «NAGImagePickerControllerCaptureImageNotification»

class NAGImagePickerController: UIImagePickerController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { override func viewDidLoad () { super.viewDidLoad () NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerViewDidLoadNotification, object: self) } override func viewWillAppear (animated: Bool) { super.viewWillAppear (animated) NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerViewWillAppearNotification, object: self) } override func viewDidAppear (animated: Bool) { super.viewDidAppear (animated) NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerViewDidAppearNotification, object: self) NSNotificationCenter.defaultCenter ().addObserver (self, selector: «flipCameras», name: kNAGImagePickerControllerFlipCameraNotification, object: nil) NSNotificationCenter.defaultCenter ().addObserver (self, selector: «captureImage», name: kNAGImagePickerControllerCaptureImageNotification, object: nil) } override func viewWillDisappear (animated: Bool) { super.viewWillDisappear (animated) NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerViewWillDisappearNotification, object: self) } override func viewDidDisappear (animated: Bool) { super.viewDidDisappear (animated) NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerViewDidDisappearNotification, object: self) } // переключаемся между передней и задней камерами func flipCameras () { cameraDevice = (cameraDevice == .Rear? .Front: .Rear) } // делаем фотографию func captureImage () { takePicture () } // сделали фотографию, теперь надо её зафиксировать и отобразить другую оверлейную вьюху func imagePickerController (picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: [NSObject: AnyObject]!) { println (info) } deinit { NSNotificationCenter.defaultCenter ().removeObserver (self) } } Теперь мы спокойно подписываемся в нашей cameraOverlayView на получение уведомлений о том, что viewDidAppear: в UIImagePickerController: NSNotificationCenter.defaultCenter ().addObserver (self, selector: «imagePickerControllerViewDidAppear:», name: kNAGImagePickerControllerViewDidAppearNotification, object: nil) Ага! Теперь можно создать элементы управления и отобразить их с анимацией: createControlElements () // единожды анимируем появление управляющих элементов — кнопок UIView.animateWithDuration (1.0, animations: { self.layout (UIDevice.currentDevice ().orientation) }) В этом же методе подпишемся на получение уведомлений об изменении ориентации девайса: UIDevice.currentDevice ().beginGeneratingDeviceOrientationNotifications () NSNotificationCenter.defaultCenter ().addObserver (self, selector: «deviceDidChangeOrientation:», name: UIDeviceOrientationDidChangeNotification, object: nil) Почему мы это делаем именно здесь и сейчас? Дело в том, что, если девайс находится при запуске приложения в произвольной ориентации — никакого первоначального уведомления об ориентации изначально не поступит, а значит в метод обработки изменений надо будет добавлять некий флаг, который бы определял, первое ли это отображение элементов или нет. Только при первом появлении проигрывается анимация. Пришлось бы добавлять ненужные ifы и вводить переменную-флаг, результат мне не понравился и я пришел к тому, что показал выше.В методе, который обрабатывает изменения ориентации устройства мы 1) располагаем кнопки в нужных углах экрана 2) анимируем поворот кнопки в сторону поворота устройства.

func layout (orientation: UIDeviceOrientation) { println (__FUNCTION__) switch orientation { case .Portrait: leftButton.frame = position (leftButton, atCorner: .UpperLeftCorner) rightButton.frame = position (rightButton, atCorner: .UpperRightCorner) case .PortraitUpsideDown: leftButton.frame = position (leftButton, atCorner: .LowerRightCorner) rightButton.frame = position (rightButton, atCorner: .LowerLeftCorner) case .LandscapeRight: leftButton.frame = position (leftButton, atCorner: .LowerLeftCorner) rightButton.frame = position (rightButton, atCorner: .UpperLeftCorner) default: // все другие варианты leftButton.frame = position (leftButton, atCorner: .UpperRightCorner) rightButton.frame = position (rightButton, atCorner: .LowerRightCorner) } var angle: CGFloat switch (prevDeviceOrientation, orientation) { case (.Portrait, .LandscapeLeft), (.LandscapeRight, .Portrait), (.PortraitUpsideDown, .LandscapeRight), (.LandscapeLeft, .PortraitUpsideDown): angle = CGFloat (M_PI) / 2.0 case (.Portrait, .LandscapeRight), (.LandscapeLeft, .Portrait), (.LandscapeRight, .PortraitUpsideDown), (.PortraitUpsideDown, .LandscapeLeft): angle = -CGFloat (M_PI) / 2.0 case (.Portrait, .PortraitUpsideDown), (.PortraitUpsideDown, .Portrait), (.LandscapeLeft, .LandscapeRight), (.LandscapeRight, .LandscapeLeft): angle = CGFloat (M_PI) default: angle = 0.0 }

let rotate = CGAffineTransformMakeRotation (angle) UIView.animateWithDuration (0.3, animations: { self.leftButton.transform = CGAffineTransformConcat (self.leftButton.transform, rotate) self.rightButton.transform = CGAffineTransformConcat (self.rightButton.transform, rotate) }) } Некоторое время я ковырялся с отрисовкой линий, никак не мог понять почему при layer.alpha = 0.0 не отображаются линии. Переклинило, установил backgroundColor в clearColor () и всё стало на свои места.Отрисовка линий находится в методе drawRect: и выглядит следующим образом:

let screenHeight = CGRectGetHeight (UIScreen.mainScreen ().bounds) let screenWidth = CGRectGetWidth (UIScreen.mainScreen ().bounds) let context = UIGraphicsGetCurrentContext () CGContextSetLineWidth (context, 1.0) CGContextSetShadow (context, CGSizeZero, 1.0) CGContextSetStrokeColorWithColor (context, UIColor (red: 0.803, green: 0.788, blue: 0.788, alpha: 0.5).CGColor) // чертим горизонтальные линии (портретный режим) let horizontalLines = screenHeight / kVisualBlocks let countHLines = screenHeight / horizontalLines for i in 1…

Программируем экран наложения фильтров После того, как пользователь сделал снимок будет вызван следующий метод: func imagePickerController (picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: [NSObject: AnyObject]!) { NSNotificationCenter.defaultCenter ().postNotificationName (kNAGImagePickerControllerUserDidCaptureImageNotification, object: self, userInfo: info) } Мы отправляем уведомление следящим объектам (слоям) о том, что фотография сделана и необходимо сменить управляющие элементы на новые (перейти на другой экран).Метод, который «обработает» уведомление выглядит следующим образом:

// анимация исчезновения управляющих элементов после того, как пользователь // сделал снимок func hideControlElements (notification: NSNotification) { // отписываем от уведомлений текущий слой для того, чтобы при анимированном // исчезновении управляющих элементов и повороте устройства пользователем // мы не обрабатывали поворот в методе deviceDidChangeOrientation NSNotificationCenter.defaultCenter ().removeObserver (self) // блокируем взаимодействие UIImagePickerController с пользователем (notification.object as NAGImagePickerController).view.userInteractionEnabled = false // прячем сетку if! superview.viewWithTag (kGridViewTag).hidden { invertGridVisibility () } // анимируем скрытие управляющих элементов UIView.animateWithDuration (1.0, animations: { self.layout (self.prevDeviceOrientation, animation: .BeforeAnimation) }, completion: { _ in let imagePickerController = notification.object as NAGImagePickerController imagePickerController.cameraOverlayView = NAGPhotoOverlayView (imageInfo: notification.userInfo, frame: self.frame) }) } Дальше я планирую поступать следующим образом: создать некий слой, который будет содержать в себе финальную фотографию, на этом слое будет еще один UIView, который будет отвечать за отображение и смену управляющих элементов.

В NAGPhotoOverlayView будет (пока) два свойства:1. UIImageView2. UIImageи объявлены следующим образом:

var photoView: UIImageView! var originalPhoto: UIImage! Инициализация происходит следующим образом:

init (imageInfo: [NSObject: AnyObject]!, frame: CGRect) { super.init (frame: frame) originalPhoto = imageInfo[UIImagePickerControllerOriginalImage] as UIImage photoView = createPhotoLayer (image: originalPhoto) addSubview (photoView) NSNotificationCenter.defaultCenter ().addObserver (self, selector: «deviceDidChangeOrientation:», name: UIDeviceOrientationDidChangeNotification, object: nil) } Подписываем текущий слой на получение уведомлений по повороту устройства для того, чтобы корректно поворачивать изображение. Хочу напомнить, что по умолчанию, фотография, которая сделана через UIImagePickerController всегда будет отображаться в портретном режиме, а это чревато сжатием изображения при отображении. Метод обработки поворотов устройства выглядит вот так:

func deviceDidChangeOrientation (notification: NSNotification) { rotateImage (toOrientation: UIDevice.currentDevice ().orientation) } основной код поворота изображения: private func rotateImage (toOrientation orientation: UIDeviceOrientation) { let orientation = UIDevice.currentDevice ().orientation let photoOrientation = originalPhoto.imageOrientation let isLandscapedPhoto = photoOrientation == .Down || photoOrientation == .Up if isLandscapedPhoto && (orientation == .LandscapeLeft || orientation == .LandscapeRight) { photoView.image = UIImage (CGImage: originalPhoto.CGImage, scale: originalPhoto.scale, orientation: orientation == .LandscapeRight? .Right: .Left) } else if! isLandscapedPhoto && (orientation == .Portrait || orientation == .PortraitUpsideDown){ photoView.image = UIImage (CGImage: originalPhoto.CGImage, scale: originalPhoto.scale, orientation: orientation == .Portrait? .Right: .Left) } } Что мы делаем? Проверим сперва в какой ориентации была сделана фотография. Если фотография в Landscape режиме, то и повороты необходимо разрешить только для положения устройства .LandscapeLeft и LandscapeRight, если фотография в портретном режиме — только .Portrait и .PortraitUpsideDown.Так как напрямую нельзя изменить свойство imageOrientation экземпляра UIImage приходится создавать новый объект.

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

Конец Просто надоело… решил статью не удалять, а всё-таки опубликовать.Исходники можно найти по этой ссылке на GitHub.

Вывод Не делайте то, в чем вы не видите смысла.Спасибо за внимание, уважаемые хабражители.

© Habrahabr.ru