22 декабря 2016 в 15:29

Секреты удачного апдейта: интерфейс, backend, структура приложения

Приближаются долгожданные новогодние праздники – время путешествий. В связи с этим сегодня мы хотели бы рассказать о работе над нашим совместным проектом с компанией Travel And Play — Webcam World View. Приложение дает возможность подключаться к стримам с видеокамер, установленных в разных уголках планеты, и наблюдать за кипящей там жизнью в реальном времени.

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




Для начала мы проанализировали текущее положение дел. Как выяснилось, у приложения имелись серьезные недостатки:

— Самое, пожалуй, большое неудобство заключалось в том, что весь контент содержался в самом приложении. Такой способ хранения исключал возможность редактирования и обновления данных без апдейта самого продукта на маркете. К тому же, это значительно увеличивало размер приложения.

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

— Доступно такое виртуальное путешествие было только пользователям Apple TV, а это не самая большая часть общества.

Осмыслив эти проблемы, мы стали искать пути их решения и в итоге поставили перед собой несколько целей:

1) Поменять интерфейс, сделать его более интуитивным;
2) Доработать серверную часть, чтобы она не только хранила весь наш контент, но и предоставляла статистику, что позволило бы нам как минимум выделять популярные и топовые камеры;
3) Ну и конечно же, адаптировать приложение для iPhone и iPad, то есть сделать виртуальные путешествия доступными для значительно большего количества людей.

Цели поставлены — настало время действовать.

Создание удобного интерфейса


Кто лучше всех знает, как сделать удобное и красивое приложение для пользователей устройств от Apple? Конечно же, Apple. Решение, которое мы искали, лежало на поверхности, а точнее на главном экране любого Apple TV — речь идет об их фирменном приложении «Фильмы». Мы захотели максимально приблизить все к этому образцу, ведь у нашего продукта схожий контент, да и UX-решение производителя всем знакомо.

Что пользователь видит первым, когда заходит в приложение «Фильмы»? Экран с подборками и топовыми фильмами. В Webcam World View мы хотели сделать примерно то же самое — все лучшие камеры на главном экране. Поэтому следующим шагом было создание аналога эппловской “карусели” на главном экране.



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

Первым делом был создан класс для ячейки нашей будущей карусели:

class TopCollectionViewCell: UICollectionViewCell {
// наполнение нашей ячейки — превью для камеры
   @IBOutlet weak var backImageView: UIImageView!

   override func awakeFromNib() {
        super.awakeFromNib()
        
        backImageView.contentMode = .scaleAspectFill
        backImageView.clipsToBounds = true
        backImageView.layer.cornerRadius = CGFloat(10.0)        
    }

  override func prepareForReuse() {
        super.prepareForReuse()
    }

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

  override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
        if (self.isFocused)
        {
      
		// анимируем движение ячейки вниз, когда элемент в фокусе
            UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: {
                self.transform = CGAffineTransform(translationX: CGFloat(0.0), y: ledgeHeight)
                }, completion: nil)

		// эффект параллакса
            self.backImageView.clipsToBounds = true
            self.backImageView.adjustsImageWhenAncestorFocused = true
        }
        else
        {
		// возвращаем элемент на исходную позицию
            UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: {
                self.transform = CGAffineTransform(translationX: CGFloat(0.0), y: CGFloat(0.0))
                }, completion: nil)

            self.backImageView.clipsToBounds = true
            self.backImageView.adjustsImageWhenAncestorFocused = false
        }

    }

Таким образом, элемент карусели имеет эффект параллакса, а при переходе в фокус принимает тот же стиль, что и элементы карусели Apple.

Далее следовала другая задача — реализация движения карусели. Для начала мы определились с тем, чего хотим.

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

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

Чтобы обеспечить возможность зацикливания карусели мы добавили «буфер» из двух ячеек, которые соответствовали первому и последнему элементу.

Переход по «внутренним» элементам был стандартным, как между элементами UICollectionView. Переход же с «внешних» элементов (первого и последнего) происходил со сдвигом контента:

 func jump(direction: Direction) {
// direction — направление сдвига, вперед или назад
        let currentOffset = self.contentOffset.x
        var offset = CGFloat(count) * (collectionViewLayout as! Layout).totalItemWidth
        if case .backward = direction {
            offset *= -1
        }
        self.setContentOffset(CGPoint(x: currentOffset + bffset, y: self.contentOffset.y),
                              animated: false)
    }

Для автоматического движения карусели был применен простой таймер и, опять же, сдвиг контента относительно начала на нужную величину:

if let initialOffset = (self.collectionViewLayout as! Layout).offsetForItemAtIndex(index: item) {
            self.setContentOffset(CGPoint(x: initialOffset, y: self.contentOffset.y), animated: animated)
 }

Так мы получили симпатичную карусель в стиле Apple с возможностью автоматического движения.

Затем мы перешли к наполнению основного пространства главного экрана.

Было решено разбить контент на подборки, они же «линейки», наподобие тех, что мы видим в приложении «Фильмы». Соответственно, встал вопрос: как их реализовать? Проблема заключалась в том, что нам нужна была возможность переходить как по подборкам (скроллинг в вертикальном направлении), так и по камерам в выбранной подборке (скроллинг в горизонтальном направлении)

Для решения этой проблемы используем TableView, в каждой ячейке которой располагаем кастомную CollectionView. В качестве делегата и источника данных выступает главный контроллер:



Чтобы иметь возможность различать коллекции разных ячеек таблицы, прибегнем свойству tag — записываем номер ячейки таблицы, в которой расположена коллекция

Для установки делегата, источника данных и текущего номера коллекции в ячейке таблицы добавляем функцию setCollectionViewDataSourceDelegate:

class LineTableViewCell: UITableViewCell {

    //заголовок подборки
    @IBOutlet weak var lineNameLabel: UILabel!
    //коллекция для подборки
    @IBOutlet weak var lineCollectionView: UICollectionView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
	   ...
    }

}
extension LineTableViewCell {
    
    func setCollectionViewDataSourceDelegate
        <D: protocol<UICollectionViewDataSource, UICollectionViewDelegate>>
        (dataSourceDelegate: D, forRow row: Int) {
        
        lineCollectionView.delegate = dataSourceDelegate
        lineCollectionView.dataSource = dataSourceDelegate
        lineCollectionView.tag = row
        lineCollectionView.reloadData()
    }
}

Теперь осталось вызвать ее в методе willDisplay таблицы:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        guard let tableViewCell = cell as? LineTableViewCell else { return }
        
        tableViewCell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.row)
    }

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

Реализация элементов других экранов была стандартной и проблем не вызвала.

Серверная часть


Итак, интерфейс был успешно реализован. Теперь пришло время рассказать о работе над серверной частью нашего приложения. Тут идея была в следующем: сделать приложение максимально настраиваемым с помощью сервера.

Вся информация о каждой камере была собрана на сервере. Для удобного модерирования контента мы написали Admin Panel. С помощью данной системы модератор может настраивать и в любой момент менять информацию о камерах, делать камеры платными или бесплатными, менять их местами и выключать, если они не работают. Любой маркетинговый ход может быть произведён без участия разработчика.

Ещё одной особенностью нашей Admin Panel стала возможность настраивать внешний вид интерфейса с сервера. В приложении есть два типа отображения данных:

1. Карусель с самыми популярными камерами.
2. Линейки с камерами — группы камер, которые выстроены в горизонтальную линию и объединены одной идеей.

При добавлении с сервера нескольких камер у модератор может объединять их в линейки вручную или же автоматически, по определённым тегам, которые добавлены для каждой камеры.

Если возникает необходимость изменить отображаемый контент или местоположение линеек, нам достаточно перетащить тот или иной элемент в нужное место.

Для создания данного блока была использован плагин jScrollPanel. jQuery + CSS — и никакой магии.

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



Admin Panel позволяет определять популярные камеры на основании собранной статистики, автоматически создает, обновляет их и перемещает внутри соответствующих линеек.

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

Timelapse


Напоследок хотелось бы поделиться еще одним решением, найденным при работе над последним апдейтом. У каждого уважающего себя приложения есть своя страничка в социальных сетях. Но выставлять там только картинки скучно, да и специфика нашего приложения намекает на то, что аудиторию нужно привлекать видеоконтентом. Видео для предпросмотра не подошли по нескольким причинам. Так родилась идея сделать экшн видео для каждого из стримов — Timelapse.

Вот как выглядит наш итоговый рецепт, создающий весьма симпатичные штуки:

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

Планы на будущее


Подведем итог. Нам удалось создать систему, с которой удобно работать даже модератору, ничего не понимающему в программировании. Теперь члены нашей команды могут вести проект, не вникая в тонкости разработки и не страдая от необходимости каждый раз обновлять приложение на маркете.

В результате мы получили добротный законченный продукт для Apple TV, в котором разрешили все поставленные задачи. Основным достижением для нашей команды стало успешное внедрение серверной части и возможности администрирования, бонусным — создание максимально понятного для пользователя интерфейса по канонам UI Apple TV.

Из этого:




Мы сделали это:





Конечно, не всё работает без сбоев и не все планы по серверу на данный момент реализованы. Код в принципе не может быть идеальным, поэтому мы стараемся не стоять на месте, постоянно работая над улучшениями. Сейчас у нас в планах реализация на сервере настройки не только контента, но и полной визуальной составляющей. Сколько будет отображаться камер в линейке на экране, будет ли это камера или Timelapse, будет ли подпись у каждой из камер или только у некоторых? Все вопросы к серверу.



Также мы подумываем улучшить внешний вид Admin Panel. Нельзя сказать, что сейчас ей неудобно пользоваться или что-то режет глаз, но всегда хочется стремиться к возможному совершенству.

Будем рады почитать ваши отзывы. На этом всё, спасибо за внимание!
Автор: @v1992
Everyday Tools
рейтинг 536,70

Комментарии (4)

  • +1
    Вместо

    self.transform = CGAffineTransform(translationX: CGFloat(0.0), y: CGFloat(0.0))
    


    обычно

    self.transform = CGAffineTransform.identity
    • 0
      Спасибо за замечание. Вы правы, учтем!
  • 0
    А IGListKit не пробовали? Они как раз под такие визуальные схемы делали для упрощения и разделения логики.
    • 0
      Нет, не пробовали. В тот момент, когда мы начали разработку этого фреймворка еще не было. Спасибо, возьмем его на заметку :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка