Задание 5. Top Places (Objective-C + iOS 9). CS 193P iOS 7 2014. Адаптивный UI. Обязательные пункты 6 -12.

Screen Shot 2016-02-08 at 1.43.20 PM
Начало решения Задания 5 (обязательные пункты 1 — 5) находится в посте.
Здесь представлено продолжение выполнения Задания 5 (обязательные пункты 6 — 12).
Текст Домашнего задания на английском языке доступен на  iTunes в пункте  “Developing iOS 7 app:Assignment 5″
На русском языке 

Задание 5 Top Places fall 2013.pdf


Задание выполнялось в Xcode 7 iOS 9. Режимы «Use Auto Layout» и «Use Size classes» в этом Домашнем задании включены.
Код Задания 5 находится на Github.

Пункт 6

Когда пользователь выбирает фотографию из любого списка, показывайте ее внутри прокручиваемого изображения (Scroll View), которое позволяем пользователю выполнять жесты pan и pinch в разумных пределах. Вы можете получить URL для  Flickr фотографии, используя метод URLForPhoto:format:  класса FlickrFetcher.

Добавляем новый View Controller на storyboard. Добавляем Scroll View и индикатор активности  Activity Indicator View (меняем его цвет на голубой). Добавляем Show Segues от прототипа ячеек таблиц Place Flickr Photos со списком фотографий и Resents c «недавними» фотографиями к новому View Controller и даем одинаковое имя этим segues (ниже будет объяснено почему). Создаем для него пользовательский класс ImageViewController,  который наследует от UIViewController:

Screen Shot 2016-01-31 at 12.58.19 PM
Пользовательский класс  ImageViewController (очень похожий на тот, что был на лекциях) имеет единственное public свойство imageURL, содержащее URL изображения фотографии:

[objc]
@property (nonatomic, strong) NSURL *imageURL;
[/objc]

Создаем outlets для ScrollView и индикатора активности spinner:

[objc]
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
[/objc]

Добавляем два других свойства для размещения imageView и самого изображения image:

[objc]
@property (nonatomic,strong) UIImageView *imageView;
@property (nonatomic,strong) UIImage *image;
[/objc]

Сделаем основные установки ScrollView внутри его setter. Зададим минимальный  minimumZoomScale и максимальный maximumZoomScale масштабы. Сделаем наш класс делегатом Scroll View и установим очень важный параметр для Scroll View — contentSize, если доступны размеры изображения image:
Screen Shot 2016-01-31 at 3.26.03 PM
Нам необходимо декларировать, что мы реализуем методы делегата UIScrollViewDelegate:

[objc]
@interface ImageViewController () <UIScrollViewDelegate>;
[/objc]

Пока нам необходим единственный метод делегата UIScrollViewDelegate, который возвращает UIView, для которого разрешается выполнять масштабирование с помощью жестов pinch и pan – в нашем случае это imageView:
Screen Shot 2016-01-31 at 3.39.41 PM
Получаем отложенно (lazily) экземпляр класса imageView (мы его не размещали на storyboard):
Screen Shot 2016-01-31 at 3.45.34 PM
и добавляем его к scrollView:
Screen Shot 2016-01-31 at 4.47.53 PM
Само по себе свойство image не запоминает изображение, а использует getter и setter для доступа к свойству image в imageView, а кроме того, «подгоняет» c помощью свойства contentSize масштаб изображения в Scroll View к размеру изображения и останавливает индикатор активности spinner:

Screen Shot 2016-01-31 at 5.00.39 PM

Каждый раз, когда устанавливается imageURL для изображения, c Flickr загружается само изображение image и размещается в свойстве self.image :

Screen Shot 2016-01-31 at 5.14.50 PM

Заголовок title и URL для изображения imageURL устанавливаются при подготовки «переезда» из ячейки таблицы со списком фотографий в методе prepareForSegue класса FlickrPhotos:
Screen Shot 2016-01-31 at 5.30.41 PM
При этом используется дополнительный метод prepareImageViewController:toDisplayPhoto:forRow подготовки фотографии к «переезду»:
Screen Shot 2016-01-31 at 5.33.12 PM
Запускаем приложение. Выбираем любое из топовых мест, затем любую фотографию, сделанную в этом топовом месте, и получаем фотографию :

Screen Shot 2016-01-31 at 5.39.25 PM

Пункт 7

Убедитесь, что заголовок фотографии находится где-то на экране при показе изображения (image) фотографии пользователю.

Выполнено.

Пункт 8

Как только изображение (image) фотографии появляется на экране, масштаб его должен устанавливаться автоматически, чтобы показать как можно больше фотографии без дополнительных, неиспользованных «белых» пространств. Как только пользователь начнет увеличивать (zoom in ) или уменьшать (zoom out) фотографию с помощью pinching жеста, вы должны прекратить автоматическое масштабирование изображения фотографии.   

Вместо того, чтобы устанавливать постоянный масштаб для изображения, его необходимо рассчитывать. Нам требуется автоматическая «подгонка» масштаба под размер изображения. Для того, чтобы «подогнать» изображение по горизонтали необходимо рассчитать масштаб по горизонтали путем деления ширины Scroll View self.scrollView.bounds.size.width на ширину изображения self.imageView.bounds.size.width. То же самое будет справедливо для вертикального масштаба, но вместо ширины with используем высоту height соответственно Scroll View и изображения image, при условии, что Scroll View не распространяется на закладку (tab bar), навигационную панель (navigation bar) и статусную панель (status bar). Высоты этих элементов необходимо учитывать при вычислении вертикального масштаба. Затем просто устанавливаем масштаб zoomScale для Scroll View в максимальный из двух рассчитанных масштабов widthRatio  и heightRatio:
Screen Shot 2016-02-03 at 8.20.31 AM
Чтобы отслеживать установку режима автоматического масштабирования, введем свойство autoZoomed:
Screen Shot 2016-02-03 at 8.44.05 AM
Как только устанавливается новое значение для изображения image (смотри приведенный выше код для setImage) мы задаем режим автоматического масштабирования

 self.autoZoomed = YES;

и сбрасываем его в методе scrollViewWillBeginZooming:withView: делегата UIScrollViewDelegate, который обнаруживает, что пользователь начал самостоятельно использовать жесты pinch и pan для изменения масштаба и перемещения изображения image:

Screen Shot 2016-02-03 at 8.55.27 AM
При любом изменение вертикального и горизонтального размера прибора (например, при вращении) необходимо пересчитывать масштаб изображения c помощью метода zoomScaleToFit:
Screen Shot 2016-02-03 at 8.58.36 AM
Такое автоматическое масштабирование позволяет выставить один из размеров (горизонтальный или вертикальный) на полный экран, то есть нет необходимости в прокручивание по нему, а другой размер можно прокручивать, если в этом есть необходимость.
Screen Shot 2016-02-03 at 9.08.04 AM

Пункт 9

В вашем приложении main thread (основной поток) никогда не должен блокироваться (например, получение данных от Flickr должно выполняться в другом потоке).

Выполнено, так как мы считываем Flickr данные в другом потоке ( не main thread).

Пункт 10

Ваше приложение должно работать как в портретной, так и в ландшафтной ориентации, как на iPhone, так и на iPad. Используйте подходящие специальные UI идиомы (например , не позволяйте вашей iPad версии выглядеть на экране как гигантская iPhone версия).

В iOS 9 будем строить адаптивный интерфейс согласно посту «Адаптивный SplitViewController и Popover для iOS 9. ( Objective-C).» Предлагается пройти следующие шаги.

Создаем новую storyboard  iPad.storyboard (не обращайте внимания на название — это будет storyboard для универсального приложения и я выбрала произвольное название, лишь бы оно отличалось от Main.storyboard) c помощью меню File -> New-> File -> Storyboard

Screen Shot 2016-02-03 at 1.09.03 PM
Копируем storyboard прямо с указанного выше поста. Убираем все, что касается Popover и кнопку URL. Переименовываем View Controllers и получаем такую storyboard:
Screen Shot 2016-02-03 at 1.17.10 PM
Заменяем пользовательские классы для View Controllers на наши:

для Top PlacesTopPlacesTVC

для  Place Flickr PhotosPlaceFlickrPhotos

для Image View Controller —  ImageViewController.

Устанавливаем новые идентификаторы для прототипов ячеек в Table View Controllers  и согласуем их с теми, что в коде:

для Top Places — «Top places»

для  Place Flickr Photos — «Flickr Photo Cel»

Устанавливаем новые идентификаторы для segues  и согласуем их с теми, что в коде:

Top Places —>  Place Flickr Photos — «Place Photos»

 Place Flickr Photos — >  Navigation Controller (Detail) — «Display Photo»

Изменяем метод prepareForSegue в классе FlickrPhotos в связи с тем, что segue типа ShowDetail не выходит напрямую на Image View Controller,  а попадает туда через  Navigation Controller (Detail):
Screen Shot 2016-02-03 at 1.36.24 PM
Кроме того мы программно добавили кнопки слева на навигационную панель: одна — возвратная

[objc]
ivc.navigationItem.leftItemsSupplementBackButton = YES;
ivc.navigationItem.leftBarButtonItem.title = self.title;
[/objc]

другая — для того, чтобы была возможность развернуть Detail на весь экран

[objc]
ivc.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
[/objc]

Мы должны добавить эти же кнопки в AppDelegate и реализовать методы делегата UISplitViewControllerDelegate, чтобы заставить  SplitViewController вести себя привычным для нас образом на приборах с Compact шириной  (все iPhones и iPhone 6+ и iPhone 6s+ в портретном режиме). Сначала мы должны подтвердить протокол UISplitViewControllerDelegate делегата и сделать себя делегатом:

Screen Shot 2016-02-03 at 2.49.28 PM
Для того, чтобы на приборах с Compact-шириной (все iPhones и iPhone 6+ и iPhone 6s+ в портретном режиме) при старте (когда imageURL = nil) появлялся Master, а не Detail (как это принято в SplitViewController) реализуем следующий метод  collapseSecondaryViewController:ontoPrimaryViewController: делегата UISplitViewControllerDelegate
Screen Shot 2016-02-06 at 4.40.17 PM
Этот метод делегата спрашивает нас, хотим ли мы при переходе в collapsed режим, чтобы Detail превратилась в Master (ведь надо оставить что-то одно)? Мы отвечаем YES только в том случае, если в Detail (которым в нашем случае является ImageViewController, вставленный в UINavigationController) нечего показывать, то есть imageURL == nil.

Теперь посмотрим внимательно на storyboard (рисунок выше). Мы видим, что Master представляет собой каскад из двух Table View Controllers. В этом случае при переходе из collapsed режима (на экране только одна таблица, например, со списком фотографий)  в expanded  режим (Master и Detail одновременно на экране)  мы получаем таблицу со списком фотографий в качестве Detail, а таблицу с топовыми местами в качестве Master. Эта ситуация появляется только тогда, когда мы работаем с iPhone 6+ и переходим из портретного режима в ландшафтный. Вы можете попробовать и убедиться в этом. Это происходит потому, что  SplitViewController по умолчанию преобразует тот View Controller, который в данный момент находится на экране и отличается от Master, в Detail, что, конечно, нарушает логику правильное функционирования для этого особого случая, когда Master представляет собой каскад из двух Table View Controllers.  Для таких особых случаев делегат UISplitViewControllerDelegate предоставляет еще один метод separateSecondaryViewControllerFromPrimaryViewController, который позволяет вам самостоятельно управлять тем, какой View Controller будет использован в качестве Detail в expanded  режиме. ( об этом подробно в посте «Адаптивный SplitViewController и Popover для iOS 9. ( Objective-C).»

Рассмотрим наш особый случай, который возникает на iPhone 6+ при переходе из портретного режима в ландшафтный при условии, чтобы мы находимся в списке фотографий. Мы должны получить следующую последовательность экранов

Screen Shot 2016-02-07 at 11.17.51 AM
Но мы ее не получим, а получим при переходе  из портретного режима в ландшафтный режим другую неправильную последовательность экранов, так как  Split View Controller вместе с  segue типа «ShowDetail» хватает» текущий View Controller (если это не Master) и превращает его в Detail.

Screen Shot 2016-02-07 at 11.31.43 AM

В нашей ситуации primaryViewController — это Navigation Controller для Master, а его топовый View Controller — это список фотографий PlaceFlickrPhotos. Вначале мы фиксируем эту особую ситуацию и получаем список фотографий  masterView

Screen Shot 2016-02-07 at 12.58.47 PM

Но мы не хотим, чтобы masterView вернулся в качестве Detail, мы хотим вернуть  ImageViewController (изображение фотографии), «завернутый» в Navigation Controller для Detail. Но нам совсем неоткуда взять Navigation Controller для  Detail , даже параметр splitViewController, указанный в методе делегата, нам не поможет, так как в данный момент он содержит всего один ViewController  и это   Navigation Controller для Master.

Остается взять  Navigation Controller для Detail  со storyboard, но это можно сделать, если задать идентификатор detailNavigation на storyboard для Navigation Controller для Detail :
Screen Shot 2016-02-07 at 10.24.59 AM
Ниже представлен код:

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Screen Shot 2016-02-07 at 1.47.04 PM
Кроме того, мы выполняем определенную настройку Detail: добавляем кнопки на навигационную панель и выставляем фотографию, указанную первой в списке фотографий.

Вот полный код метода separateSecondaryViewControllerFromPrimaryViewController делегата  UISplitViewControllerDelegate:
Screen Shot 2016-02-03 at 4.50.14 PM
Теперь UISplitViewController функционирует правильно на любых устройствах, включая iPhone 6+ и iPnone 6s+.
Добавляем Tab Bar Controller вместе с другой закладкой  Resents
Screen Shot 2016-02-05 at 5.00.44 PM
Пробуем запустить приложение и обнаруживаем, что на iPad все работает нормально, а на всех iPhone в Compactwidth режиме ImageViewController  представляется на экране модально с заголовком, но без навигационной панели и без дополнительных кнопок на ней и мы не можем никуда с этого экрана двинуться:
Screen Shot 2016-02-07 at 3.26.34 PM
Это говорит о том, что SplitViewController не может поместить наш ImageViewController в стэк Navigation Controller для Master, так как SplitViewController управляет Master через Tab Bar Controller, который и предлагает модальное представление Detail. Чтобы исправить эту ситуацию, мы можем сами поместить ImageViewController в стэк Navigation Controller для Master и будем это делать в методе showDetailViewController: sender: делегата UISplitViewControllerDelegate для Compactwidth режима.

Сначала определяем Compactwidth режим и получаем Navigation Controller для Master:
Screen Shot 2016-02-07 at 3.54.25 PM
.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
затем извлекаем ImageViewController и помещаем его в стэк Navigation Controller для Master и показываем его на экране:
.  .  .  .  .  .  .  .  .  .  .  .  .  .  . .  .  .  .  .  .  .  .  .  .  .
Screen Shot 2016-02-07 at 4.00.51 PM
Возврат YES говорит о том, что SplitViewController не должен управлять показом Detail, мы сами будем его показывать. Теперь изображение фотографии для Compactwidth режима будет показываться правильно: c возвратной кнопкой.

Но коль скоро мы добавили ImageViewController в стэк Navigation Controller для Master, то можем получить наш  ImageViewController в Master при переходе в ландшафтный режим:
Screen Shot 2016-02-07 at 4.15.33 PM
При переходе в ландшафтный режим нам нужно убрать ImageViewController из стэка Navigation Controller для Master:

Screen Shot 2016-02-07 at 4.27.26 PM
Пользуясь знанием imageURL только что убранного ImageViewController, можем настроить соответствующую строчку в списке фотографий:

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Screen Shot 2016-02-07 at 4.46.58 PM
В результате ситуация на iPhones поправится, включая и переход от портретного режима к ландшафтному на iPhone 6+:

Screen Shot 2016-02-07 at 4.22.29 PM
Теперь все работает правильно на всех приборах.

Пункт 11

Список “недавних” фотографий должен сохраняться в NSUserDefaults (то есть они должны постоянно сохранятся между запусками данного приложения). Будет более удобно, если массивы, которые возвращаются с Flickr, все были бы property lists (сразу же после преобразования из формата JSON).

Создаем новый класс RecentsUserDefaults, в котором будет два метода класса: один для сохранения массива фотографий в NSUserDefaults, а другой — для извлечения массива фотографий из NSUserDefaults:

Screen Shot 2016-02-07 at 5.06.40 PM
Запоминать только что просмотренную фотографию будем в методе prepareImageViewController: toDisplayPhoto: forRow: класса FlickrPhotos:

Screen Shot 2016-02-07 at 5.27.33 PM
Для показа «недавних» фотографий создадим класс ResentsTVC, который наследует от класса FilckrPhotos и единственной работой которого будет загрузка фотографий из NSUserDefaults:

Screen Shot 2016-02-07 at 5.59.57 PM
Screen Shot 2016-02-07 at 6.02.03 PMТаким образом мы использовали подсказку № 9:

Используйте объектно-ориентированное программирование для инкапсуляции функциональности в вашем приложении. Допустим, что в вашем приложении вам понадобится 5 различных  UITableViewController subclasses. Вы могли бы подходящим образом “разделить” (share) код между ними, и получить прекрасно сконструированные, повторно используемые Controller с понятными API и Model. И это совершенно правильно: вначале создать UITableViewController subclass, который будет что-то делать, а затем создать subclass вновь полученного класса для того, чтобы делать уже что-то более изысканное.

Пользовательский класс ResentsTVC устанавливаем на storyboard Resents:

Screen Shot 2016-02-07 at 6.09.07 PM
Запускаем приложение, выбираем закладку Resents, получаем список «недавно» просмотренных фотографий и можем получить их изображение:

Screen Shot 2016-02-07 at 6.55.18 PM
Но для того, чтобы наша адаптивном приложение работало бы для ResentsTVC, нам нужно внести некоторые совсем небольшие изменения в метод separateSecondaryViewControllerFromPrimaryViewController делегата UISplitViewControllerDelegate. Для того, чтобы этот код выполнялся для любого класса, который наследует от класса FlickrPhotos, мы должны заменить наш конкретный класс PlaceFlickrPhotos на базовый класс FlickrPhotos:

Screen Shot 2016-02-07 at 7.15.17 PM
. . . . . . . . . . . . . . . . . . .
Теперь этот код выполняется для любого класса, наследующего от класса FlickrPhotos (класс PlaceFlickrPhotos и ResentsTVC ).

Продолжая использование объектно-ориентированного программирования для инкапсуляции функциональности в нашем приложении, высказанные в подсказке №9 (текст приведен выше), давайте сделаем класс TopPlacesTVC более обобщенным классом (наподобие FlickrPhotos), отображающим топовые места Flickr независимо от того, как они были получены (например из NSUserDefaults), а само получение топовых мест из сети перенесем в класс CurrentTopPlacesTVC.
В классе TopPlacesTVC останется public API в виде массива топовых мест;
Screen Shot 2016-02-08 at 6.29.23 AM
а в класс CurrentTopPlacesTVC перенесем получение топовых мест с сервера Flickr.com
Screen Shot 2016-02-08 at 6.35.16 AM
Обратите внимание на то, что метод fetchTopPlaces превращен в IBAction простой заменой void на IBAction. Это в дальнейшем нам поможет практически бесплатно получить Refresh Control. Для этого идем на storyboard и включаем Refresh Control.
Screen Shot 2016-02-08 at 6.44.08 AM
Затем с помощью CTRL-перетягивания подключаем Refresh Control к коду:
Screen Shot 2016-02-08 at 7.01.15 AM
Осталось добавить старт и остановку Refresh Control, при этом остановка Refresh Control потребует вернуться в main queue:
Screen Shot 2016-02-08 at 6.35.16 AM
Не забудьте поменять на storyboard пользовательский класс на CurrentTopPlacesTVC:
Screen Shot 2016-02-08 at 7.14.31 AM
Теперь работает Refresh Control:
Screen Shot 2016-02-08 at 7.20.43 AM
То же самое нужно сделать и со списком фотографий PlaceFlickrPhotos:
Screen Shot 2016-02-08 at 7.28.26 AM
Но здесь нам придется учесть замечание № 16:

Оказывается, что UIRefreshControl не всегда появляется, если вы вызываете beginRefreshing программным путем. Если вы бесстрашны, то можете это доработать заставив UITableView подкручиваться вверх путем установки его contentOffset (помните, что UITableView является UIScrollView) отрицательного значения координате y (установка ее равной высотеheight refreshControlбыло бы лучшим вариантом). Однако эта доработка не является Обязательным заданием (старт refreshControl все равно происходит, даже если его не подкрутить чуть-чуть наверх, чтобы он появился).

В код необходимо добавить одну строку:
Screen Shot 2016-02-08 at 7.38.06 AM
Теперь все работает. Код на Github.

Пункт 12

Ваше приложение должно работать на реальном устройстве.

Попробуйте сами. Все должно работать.