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

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



    Описание проекта


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

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

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


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


    Прототипы. Раз.


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

    Какие наброски были у меня:



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

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

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

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


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

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


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




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


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

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




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

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





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




    Сравните:




    Сравните:




    Может попробовать сделать так?



    А в портрет режиме перенесём элементы вниз:


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




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

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


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

    Упростил до такого:


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

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






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


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


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

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

    Наброски в Keynote:






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

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


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

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


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



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

    Прототипы: отзывы. Два.


    Экран авторизации:

    Комментарии: отсутствуют.

    Модальное окно авторизации в ВК (корректированная версия):

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


    Окно съемки (портретный режим):

    Комментарии: отсутствуют.

    Окно съемки (лэндскейп режим):

    Комментарии: отсутствуют.

    Фильтры (портретный режим):

    Комментарии: отсутствуют.

    Фильтры (лэндскеп режим):

    Комментарии: отсутствуют.

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

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


    Настройки загрузки (лэндскейп режим):

    Комментарии: такие же, как к экрану в портретном режиме.

    Прототипы: анимация. Три.


    Видео можно просмотреть по этой ссылке.

    Программируем главный экран.


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

    Начнем с того, что использование блюра в чистом виде не даст нужного эффекта.

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

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

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

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

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

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


    Отсчет идёт против часовой стрелки, в реализации же 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
    


    Можете сравнить (слева макет, справа реализация):


    Программируем экран авторизации в ВК.


    Первое, что необходимо сделать — интегрировать 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)
    



    После успешной авторизации получим токен доступа, который будет сохранён в хранилище данных SDK. Пользователя перекинет на экран фотографии.

    Программируем экран реализации фотографии


    Начну с того, что покажу итоговые результаты:



    Сетка убирается и сделана такой незаметной специально.

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


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

    За основу я взял 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)
    

    Небольшим сюрпризом для меня оказалось появление вот этого визуального элемента зума:


    Он мне здесь не нужен был, да и портил очень вид, к тому же перекрывал кнопку — надо убирать. Реализовал первый способ, который пришел в голову:
        let pinchGR = UIPinchGestureRecognizer(target: self, action: nil)
        addGestureRecognizer(pinchGR)
    


    Потом решил поискать на SO какие-то более интересные варианты решения и нашел — установка userInteraction в false.

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


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

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

    Первый вариант я почти начал пробовать и смотреть, но в какой-то момент просто стошнило от этого решения и я счел его уродским. В итоге всё было реализовано с использованием уведомлений. Получилось достаточно просто и в то же время гибко.
    Вот так выглядит на данном этапе NAGImagePickerController:
    //
    //  NAGImagePickerController.swift
    //  Qtty
    //
    //  Created by AndrewShmig on 21/07/14.
    //  Copyright (c) 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..<countHLines {
          CGContextMoveToPoint(context, 0, i * horizontalLines)
          CGContextAddLineToPoint(context, screenWidth, i * horizontalLines)
        }
        
        // чертим вертикальные линиии (портретный режим)
        let verticalLines = screenWidth / kVisualBlocks
        let countVLines = screenWidth / verticalLines
        for i in 1..<countVLines {
          CGContextMoveToPoint(context, i * verticalLines, 0)
          CGContextAddLineToPoint(context, i * verticalLines, screenHeight)
        }
        
        CGContextStrokePath(context)
    


    После реализации экрана, в блоге Swift, появилась информация о модификаторах доступа.
    Актуальный исходный код приложения можно найти на GitHub'е, в конце статьи смотрите ссылку на репозиторий.

    Программируем экран наложения фильтров


    После того, как пользователь сделал снимок будет вызван следующий метод:
      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. UIImageView
    2. 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.

    Вывод


    Не делайте то, в чем вы не видите смысла.

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 18

      +3
      Вывод нужно написать в начале статьи.
        0
        Так не интересно!
          0
          Или в заголовок
          +7
          Читал, читал, надоело. Решил вкладку не закрывать, а пролистать до комментариев.
            +1
            О боги, только ленивый не делал своё приложение камеры и эффектов для AppStore. Таких приложений сотни тысяч уже, и десятки в день раздают бесплатно. Угомонитесь уже делать свои инстраграмы.
              –1
              О боги, вы бы хоть техническую часть прочитали… может по ней бы какие-то советы возникли.
                –1
                Мой совет, никогда не начинайте делать приложение не проверив есть ли в этом необходимость.
                  0
                  Вам знакомо такое понятие, как for fun?
              +1
              Можно я вас стукну за расположение кнопок в портретном режиме вверху? Возьмите в руку телефон и попробуйте их нажать той же рукой. И да в русском языке «авторизация» или «авторизоваться», звучит не очень приятно. Есть отличные и удобные «вход» и «войти».
                0
                Момент спорный с кнопками. В лэндскейп режиме мне очень удобно было + есть некая связь с тем, как работают с реальными фотоаппаратами:
                image

                А насчет кнопки «Авторизоваться» — вы правы, «Войти» лучше звучит.
                  0
                  Момент спорный с кнопками. В лэндскейп режиме мне очень удобно было + есть некая связь с тем, как работают с реальными фотоаппаратами

                  Еще раз процитирую себя:
                  Можно я вас стукну за расположение кнопок в портретном режиме вверху? Возьмите в руку телефон и попробуйте их нажать той же рукой.

                  А теперь поясните зачем вы мне фотографию с ландшафтным режимом показываете? :)
                    0
                    Ответил ниже :)
                  0
                  А что касается портретного режиме, опять таки с 6 айфоном одной рукой дотянуться будет тоже не очень удобно (см картинку сверху), поэтому расчет делался на фотоаппаратный стиль реализации снимков.
                    0
                    Если расположите кнопки в нижнюю часть, все будет ок. Причем дополнительно можно сделать режим левша-правша и располагать кнопки по одному из краев.
                      0
                      Насчет расположения кнопок внизу — думал, возможно это лучший вариант.
                      Что касается для левшей и правшей — взгляните на один из экранов фильтров, когда часть фильтров под левой рукой, а часть под правой. Да, есть над чем экспериментировать.
                  0
                  статья интересная, проект напомнил мне идею сервиса создания единых фонов рабочих столов на разные устройства: ноутбук, смартфон, планшет [задается фон, текст, картинка и все это размещается автоматически с адаптацией под конкретное разрешение — doto списки, мотиваторы]. в итоге пришел к выводу, что обои рабочего стола на смарте/планшете практически не используются [все пространство рабочего стола активно используется]
                    0
                    Вот и мне кажется, что использование множества управляющих элементов, которые уменьшают размер самого изображения — не айс.
                    +1
                    впечатление, что угол отсчитывается не в ту сторону, складывается потому, что в экранных координатах ось у направлена вниз, а не вверх, как в учебниках.

                    Only users with full accounts can post comments. Log in, please.