PreWorking 1.3: Паттерны — часть 2

P

Swift 2.2
Мы не останавливаемся и продолжаем готовить вас к будущему собеседованию по iOS разработке. Это вторая часть практической темы — паттерны. Первая честь здесь. Тема эта самая тяжелая в разделе и самая ключевая. А так же она требует некоторых навыков создания iOS приложений. Если вы только начинаете свой путь  iOS разработчика — пропустите эту статью и пока что ознакомьтесь только с теорией по ссылкам в конце. Спустя пару разделов вы сможете вернуться и пройти практическую часть этой статьи. Все написанное является переводом статьи Ray Wenderlich. Итак, продолжим.   

1. Общая информация:

     1.1 ООП

     1.2 Принципы проектирования

     1.3 Паттерны — часть 1 — часть 2

     1.4 Системы контроля версии

     1.5 Использование third-party решений в iOS разработке

 

В этой части на примере оставшихся паттернов мы научим приложение сохранять данные в файл, качать данные из сети и много чего еще!

Паттерны: Шаблон проектирования Adapter

Адаптер позволяет классам с несовместимыми интерфейсами работать вместе. Адаптер — это обёртка вокруг объекта, которая предоставляет унифицированный интерфейс для взаимодействия с этим объектом.

Если вы знакомы с паттерном «Адаптер» по другим языкам, то вы заметите, что Apple реализует его своеобразно (примерно как и требование Еврокомиссии перейти на MicroUSB). Они это делают с помощью протоколов. Вы, можете быть, знакомы с протоколами UITableViewDelegate, UIScrollViewDelegate, NSCoding, NSCopying. К примеру, протокол NSCopying предоставляет любому классу стандартный метод copy.

Паттерны: Как использовать Adapter

Горизонтальный скрол для листания альбомов будет выглядеть следующим образом.

Паттерны

Чтобы приступить к его реализации, создайте новый класс из шаблона Cocoa-Touch class (ещё не забыли, как?), назовите его HorizontalScroller и наследуйте его от UIView.

Откройте HorizontalScroller.swift и добавьте следующий код выше объявления класса:

Этим мы определяем протокол HorizontalScrollerDelegate — наследник от протокола NSObject (таким же образом, как наследуются классы). Общепринятая практика — чтобы класс соответствовал протоколу NSObject или другому протоколу, который наследуется/соответствует протоколу NSObject.

Объявляем обязательные и необязательные методы, которые делегат должен реализовать (объявляем в теле протокола):

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

В HorizontalScroller.swift добавьте следующий код после определения класса HorizontalScroller:

Заметьте, мы добавили атрибут weak («слабый») к свойству (указателю на) delegate. Это необходимо, чтобы предотвратить «retain-зацикливание». Если объект хранит сильный указатель на делегата, а делегат хранит сильный указатель обратно на тот объект, то вы гарантированно получите утечку памяти. Ни один из двух объектов никогда не освободит память.

Добавьте в клас еще свойства:

По пунктам, что мы делаем:

  1. Определяем константы для удобной модификации внешнего вида на этапе разработки. Размеры представлений внутри скролла будут 100х100 пунктов с отступом 10 пунктов от прямоугольника, который их обрамляет.
  2. Создаем ScrollView, содержащий альбомы
  3. Создаем массив обложек.

Далее добавляем инициализаторы:

Инициализаторы делегируют большую часть работы к initializeScrollView(). Вот что происходит в этом методе:

  1. Создаем новый экземпляр UIScrollView и добавляет его к родительскому представлению.
  2. Выключаем маски автоизменения размеров.
  3. Применение ограничения к scrollview. Мы хотим, чтобы ScrollView полностью заполнило HorizontalScroller.
  4. Создаем tap gesture recognizer. Tap gesture recognizer распознает жест касанияк ScrollView и проверяет, коснулись ли мы обложки.

Добавьте еще один метод:

Жест передается в качестве параметра и позволяет нам извлечь расположение с locationInView().
Затем мы вызываем numberOfViewsForHorizontalScroller() в делегате. У экземпляра HorizontalScroller нет информации о делегате кроме знания, что он может безопасно отправить это сообщение, так как делегат должен соответствовать протоколу HorizontalScrollerDelegate.
Добавьте следующий, чтобы получить доступ к обложке альбома от скроллера:

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

Для перезагрузки скроллера добавьте следующий код:

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

  1. Проверки на делегат, прежде чем мы выполним любую перезагрузку.
  2. Так как вы очищаете обложки альбомов, вы также должны сбросить viewArray.
  3. Удалите все подпредставления, ранее добавленные к ScrollView.
  4. Все представления запускаются с заданным смещением. В настоящее время это 100, но это можно легко настроить, изменив постоянный VIEW_OFFSET в начале.
  5. HorizontalScroller спрашивает свой делегат о всех view по одному, и он кладет их рядом горизонтально с ранее определенным отступом.
  6. Сохранение представлений в viewArray.
  7. Как только все представления существуют, установите смещение содержания для HorizontalScroller позволяет пользователю просматривать все альбомы путем прокрутки.
  8. HorizontalScroller проверяет, реализует ли его делегат initialViewIndex(). Эта проверка необходима, потому что этот метод протокола дополнительный. Если делегат не реализует этот метод, 0 используется в качестве значения по умолчанию.

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

didMoveToSuperview вызывается, когда HorizontalScroller добавлен к другому представлению как подпредставление. Это — правильное время, чтобы перезагрузить содержание скроллера.
Последняя часть проблемы — HorizontalScroller должty удостовериться, что альбом, который вы просматриваете, всегда центрируется в scrollView. Чтобы сделать это, вы должны будете выполнить некоторые вычисления.

Этот код помогает центрировать альбом. Как только представление центрируется, мы сообщаем делегату, что выбранное представление изменилось.

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

scrollViewDidEndDragging (_: willDecelerate:) сообщает делегату, когда пользователь заканчивает перетаскивать. Параметр decelerate равен true, если представление не остановилось. Вы должны вызвать новый метод, чтобы центрировать текущее представление, так как текущее представление, вероятно, изменилось после того, как пользователь перетащил ScrollView.
В initializeScrollView() добавляют следующий код после scroller = UIScrollView():

HorizontalScroller готов к использованию. Откройте Storyboard и войдите в identity inspector. Установите для скрола класс HorizontalScroller.

Паттерны Далее свяжите этот скролл с Outlet в ViewController.swift и назовите его scroller:

Паттерны

Далее в файле ViewController.swift мы должны реализовать некоторые методы HorizontalScrollerDelegate. В конце файла добавьте следующее расширение.

  1. Берем раннее выбранную обложку альбома и «отменяем» выбор обложки.
  2. Сохраняем текущий индекс обложки.
  3. «Захват» выбранной обложки.
  4. Выводим на экран данные в Table View.

Добавьте следующий метод в расширение:

Этот код возвращает количество View для ScrollView. Так как ScrollView выведет на экран обложки для всех альбомов, количество — число записей.

Напоследок, добавьте в расширение следующий код:

Здесь мы создаем новый AlbumView, затем проверяем, выбрал ли пользователь этот альбом. Наконец, мы передаете его к HorizontalScroller.
Вот именно! Только три сокращенных метода вывести на экран хорошо выглядящий горизонтальный скроллер.
Добавить следующий метод в класс:

Этот метод загружает данные альбома через LibraryAPI и затем устанавливает выведенное на экран представление на основе индекса текущего представления. Если индекс текущего представления — меньше чем 0, это значит что никакое представление не было в настоящее время выбрано.

Добавьте следующие строки во viewDidLoad():

Так как HorizontalScroller создавался в Storyboard, все, что мы должны сделать — установить делегат, и вызовать reloadScroller(), который загрузит Subview для скроллера, чтобы вывести на экран данные альбома.

Запустите программу и узрите ваш горизонтальный скроллер =)

Круто, скроллер работает. Но где же обложки?

Паттерны

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

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

Ребус? Ни сколько! В этой простейшей задачке нам поможет Observer.

Паттерны: Шаблон проектирования Observer

В паттерне Наблюдатель один объект уведомляет другие объекты о любых изменениях состояния. Объекты не должны знать о друг друге. Этот паттерн чаще всего используется для уведомления нужных объектов об изменении каких либо свойств.
Обычная реализация требует, чтобы наблюдатель зарегистрировал интерес к состоянию другого объекта. Когда изменится состояния, все объекты наблюдения будут уведомлены относительно изменения.
Если вы хотите придерживаться паттерны MVC (что мы и делаем в этом проекте), вы должны позволить объектам Модели связаться с объектами Представления, но без прямых ссылок между ними. И это то, где можно применить Observer.
Cocoa реализует шаблон наблюдатель двумя примечательными способами: Notifications и Key-Value Observing (KVO).

Notifications

Не путайте понятие с Push или Local уведомлениями. Нотификации работают по принципу Лектор-Слушатели. Объект-Лектор отправляет сообщения объектам-Слушателям. Лектор ничего не должен знать о Слушателях.

Перейдите к AlbumView.swift и введите следующий код в конце инициализатора init(frame: CGRect, albumCover: String):

Этот код отправляет нотификацию через синглтон NSNotificationCenter. Уведомление содержит информацию о UIImageView для заполнения и URL обложки для альбома, которая будет загружена. Это вся необходимая для загрузки  информация.

Добавьте следующик код в LibraryAPI.swift в init сразу после super.init():

Это другая сторона управления Observer. Каждый раз, когда AlbumView отправит уведомление BLDownloadImageNotification, система будет уведомлять об этом LibraryAPI. В ответ на уведомление LibraryAPI вызывает downloadImage(). Прежде чем реализовывать функцию загрузки, давайте сделаем необходимое действие — отпишемся от нотификации, когда объект деинициализирован. Ведь если послать уведомление в несуществующий объект, это может плохо кончится для приложения. Добавьте к LibraryAPI метод:

Теперь давайте реализуем одну хорошую задумку — локальное сохранение загруженных фото. Это избавит от необходимости загружать их много раз. Откройте PersistencyManager.swift и добавьте методы:

Это простой код. Загружаемые фото будут сохранены в папке Documents и getImage() вернет nil, если фото не будет найдено в этой папке.

Теперь добавьте метод в LibrariAPI.swift:

  1. downloadImage выполняется с помощью нотификации, получая объект нотификации как параметр. UIImage и URL получены из нотификации.
  2. Берем изображение из PersistentManager, если оно было загружено ранее.
  3. Если оно загружено не было, то скачиваем его через HTTPClient.
  4. Когда изображение загружено, выводим его на экран и используем PersistentManager для сохранения локально.

Перед тем как запускать приложение, давайте позволим приложению работать через незащищенное соединение. Для этого перейдите в info.plist и добавьте строку, нажав на + в шапке документа: NSAppTransportSecurity. Раскройте его и добавьте Allow Arbitrary Loads со значением YES.
Паттерны

Запускаем программу. Все работает!

Паттерны

Обратите внимание на крутящийся кружочек на альбомах — Spinner. Мы не реализовали логику его остановки по окончанию загрузки. В этом нам поможет иная сторона Observer — KVO.

Key-Value Observing (KVO)

В KVO объект может попросить быть уведомленным относительно любых изменений в определенном свойстве: его собственном или тот из другого объекта. Если Вам интересно, Вы можете читать больше об этом в руководстве по программированию Apple KVO.

Как упомянуто выше, механизм KVO позволяет объекту наблюдать за изменениями в свойстве. В нашем случае мы можем использовать KVO, чтобы наблюдать изменения в свойстве изображения UIImageView, которое содержит изображение.
Откройте AlbumView.swift и добавьте следующий код в init(frame:albumCover:) сразу после commonInit():

Этот код добавляет текущий класс как наблюдатель для свойства изображения coverImage. Добавьте следующий метод для того что бы отписаться при деинициализации.

И наконец, добавьте следующий метод:

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

На данный момент приложение не сохраняет свое состояние после перезагрузки. Каждый раз выбран первый альбом. Нам нужно заставить сохранять последнюю выбранную позицию. И в этом нам поможет Memento.

Паттерны: Шаблон проектирования Memento

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

Добавьте слежующие методы в ViewController.swift:

saveCurrentState сохраняет текущий индекс альбома к NSUserDefaults. NSUserDefaults — стандартное хранилище данных, обеспеченное iOS для того, чтобы сохранить некоторые настройки и данные.
loadPreviousState загружает ранее сохраненный индекс. Это не полное осуществление паттерна memento, пока что.

Теперь, Добавьте следующую строку к viewDidLoad в ViewController.swift перед scroller.delegate:

Это загружает ранее сохраненное состояние, когда приложение запускается. Но где мы сохраним текущее состояние приложения для загрузки? Мы будем использовать нотификации, чтобы сделать это. iOS отправляет уведомление UIApplicationDidEnterBackgroundNotification, когда приложение уходит в фон. Вы можете использовать это уведомление, чтобы вызвать saveCurrentState. Удобно же?
Добавьте следующую строку в конце viewDidLoad:

Теперь, когда приложение уйдет в фон, ViewController автоматически сохранит текущее состояние, вызывая saveCurrentState.

Но не забываем вызывать отписываться от нотификаций при деинициализации объекта:

Запустите приложение, выберите какой либо альбом, и с помощью комбинации в симуляторе Command+Shift+H сверните приложение в фон. В этот момент сработает метод сохранения состояния. Что бы убедится что оно сохранено, перезапустите приложение в Xcode. Вы увидите, что все работает правильно, однако скроллер при старте не переместился за сохраненным альбомом. Вот для этого нам и нужен был опциональным метод initialViewIndexForHorizontalScroller. Добавьте его во ViewController.swift:

Теперь перезапустите приложение и увидите, что все работает правильно!

Если вы посмотрите на init в PersistentManager, то вы заметите, что альбомы создаются каждый раз, когда PersistentManager создается. Но лучше создать список альбомов один раз и сохранить их в файле. Как вы сохранили бы данные Album в файле?
Используем созданный Apple механизм архивации!

Архивация

Одна из функций паттерна Memento — архивация. Она позволяет преобразовать объект в «поток», который может быть сохранен и позже восстановлен.

Сначала вы должны объявить, что Album может быть заархивирован, то есть соответствует протоколу NSCoding. Откройте Album.swift и измените строку объявления класса:

После этого добавьте два метода в сам класс:

Как часть протокола NSCoding, encodeWithCoder будет вызван, когда мы попросим экземпляр Album быть заархивированными. С другой стороны,инициализатор  init(coder:) будет использоваться, чтобы восстановить или разархивировать сохраненный экземпляр. Это просто, но все же мощно.
Теперь, когда класс Album может быть заархивирован, добавьте код, который фактически сохраняет и загружает список альбомов.

Добавьте следующий метод в PersistencyManager.swift:

Это метод, который вызывают, чтобы сохранить альбомы. NSKeyedArchiver архивирует массив альбома в файл, названный albums.bin.
Когда Вы архивируете объект, который содержит другие объекты, archiver автоматически пытается рекурсивно заархивировать дочерние объекты и любые дочерние объекты дочерних объектов и т.д. В этом экземпляре архивация запускается с альбомов, которые являются массивом экземпляров Album. Так как Array и Album оба поддерживают интерфейс NSCopying, все в массиве автоматически архивируется.

Сейчас замените init в PersistentManager.swift на этот код:

Мы переместили код создания альбомов в отдельный метод createPlaceholderAlbum(). В новом коде NSKeyedUnarchiver загружает данные альбома из файла, если он существует. Если не существует, то создает данные альбома и сразу сохраняет его для следующего запуска приложения.
Нам также нужно сохранить данные альбома каждый раз, когда приложение входит в фон. Добавьте следующий код в LibraryAPI.swift:

Этот код просто передает сохранение альбомов в LibraryAPI. Добавьте теперь строку кода в конце saveCurrentState во ViewController.swift:

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

К сожалению, нет никакого простого способа, чтобы проверить, корректна ли персистентность данных. Вы можете проверить папку Documents в папке приложения в Finder, чтобы видеть, что файл данных альбома создается, но чтобы видеть любые другие изменения, вы должны будете добавить возможность изменить данные альбома.
Но вместо того, чтобы изменить данные, что если мы добавим опцию удаления альбомов?, которые больше не хотим держать в своей библиотеке? Кроме того, не было бы лишним иметь опцию отмены, если удаляен альбом по ошибке.

Паттерны: Финальные штрихи

Мы собираемся добавить последние штрихи к музыкальному приложению, позволяющие пользователю выполнить действие удаления, чтобы удалить альбом или действия отмены в случае, если он передумал. Добавьте следующее свойство во ViewController:

Этот код создает пустой стек отмены. undoStack будет содержать кортеж двух параметров. Первым является Album, и вторым является индекс альбома.
Добавьте следующий код после reloadScroller() в viewDidLoad:

Этот код создает панель инструментов с двумя кнопками и гибким пространством между ними. Кнопка отмены отключена, потому что стек отмены еще пустой. Обратите внимание на то, что панель инструментов уже находится в Storyboard, таким образом все что мы должны сделать — установить элементы панели инструментов.
Вы добавите три метода к ViewController.swift для обработки действий управления альбомом: добавление, удаление, и отмена.

Для начала добавим метод добавления альбома:

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

Далее идет метод удаления, но в нем все по сложнее:

  1. Удаляем альбом.
  2. Создаем переменную, названную undoAction, которая хранит кортеж Альбома и его индекса. Затем добавляем кортеж в стек.
  3. Используйте LibraryAPI, чтобы удалить альбом из структуры данных и перезагрузить скроллер.
  4. Мы должны включить кнопку отмены.
  5. Проверяем, есть ли еще какие либо альбомы. Если нет — выключаем кнопку удаления.

Наконец то, добавьте метод отмены:

  1.  Метод “выталкивает” объект из стека, давая кортеж, содержащий Альбом и его индекс. Их вы используете для возвращения альбома.
  2. Так как мы также удалили последний объект в стеке, когда “выталкивали” его, мы теперь должны проверить, пуст ли стек. Если да, то это означает что больше нет действий для отмены. Таким образом, мы отключаете кнопку Undo.
  3. Мы также знаем, что так как мы отменили действие, должна быть по крайней мере одна обложка альбома. Следовательно мы включаем кнопку удаления.

Запустите приложение и поиграйтесь с ним. Вы увидите, что удаление/возвращение работают отлично.

Паттерны

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

Финальное приложение вы можете скачать по этой ссылке!

Паттерны: Полезные ссылки для углубления в тему

Поддержите ресурс blog.justDev:

Сведения об авторе

Игорь Малеваный

Добавить комментарий

Instagram

Поддержите ресурс blog.justDev:

Свежие записи

Рубрики