Начало решения Задания 5 (обязательные пункты 1 — 5) находится в посте.
Здесь представлено продолжение выполнения Задания 5 (обязательные пункты 6 — 12).
Текст Домашнего задания на английском языке доступен на iTunes в пункте “Developing iOS 7 app:Assignment 5″.
На русском языке
Задание выполнялось в 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:
Пользовательский класс 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:
Нам необходимо декларировать, что мы реализуем методы делегата UIScrollViewDelegate:
[objc]
@interface ImageViewController () <UIScrollViewDelegate>;
[/objc]
Пока нам необходим единственный метод делегата UIScrollViewDelegate, который возвращает UIView, для которого разрешается выполнять масштабирование с помощью жестов pinch и pan – в нашем случае это imageView:
Получаем отложенно (lazily) экземпляр класса imageView (мы его не размещали на storyboard):
и добавляем его к scrollView:
Само по себе свойство image не запоминает изображение, а использует getter и setter для доступа к свойству image в imageView, а кроме того, «подгоняет» c помощью свойства contentSize масштаб изображения в Scroll View к размеру изображения и останавливает индикатор активности spinner:
Каждый раз, когда устанавливается imageURL для изображения, c Flickr загружается само изображение image и размещается в свойстве self.image :
Заголовок title и URL для изображения imageURL устанавливаются при подготовки «переезда» из ячейки таблицы со списком фотографий в методе prepareForSegue класса FlickrPhotos:
При этом используется дополнительный метод prepareImageViewController:toDisplayPhoto:forRow подготовки фотографии к «переезду»:
Запускаем приложение. Выбираем любое из топовых мест, затем любую фотографию, сделанную в этом топовом месте, и получаем фотографию :
Пункт 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:
Чтобы отслеживать установку режима автоматического масштабирования, введем свойство autoZoomed:
Как только устанавливается новое значение для изображения image (смотри приведенный выше код для setImage) мы задаем режим автоматического масштабирования
self.autoZoomed = YES;
и сбрасываем его в методе scrollViewWillBeginZooming:withView: делегата UIScrollViewDelegate, который обнаруживает, что пользователь начал самостоятельно использовать жесты pinch и pan для изменения масштаба и перемещения изображения image:
При любом изменение вертикального и горизонтального размера прибора (например, при вращении) необходимо пересчитывать масштаб изображения c помощью метода zoomScaleToFit:
Такое автоматическое масштабирование позволяет выставить один из размеров (горизонтальный или вертикальный) на полный экран, то есть нет необходимости в прокручивание по нему, а другой размер можно прокручивать, если в этом есть необходимость.
Пункт 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
Копируем storyboard прямо с указанного выше поста. Убираем все, что касается Popover и кнопку URL. Переименовываем View Controllers и получаем такую storyboard:
Заменяем пользовательские классы для View Controllers на наши:
для Top Places — TopPlacesTVC
для Place Flickr Photos — PlaceFlickrPhotos
для 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):
Кроме того мы программно добавили кнопки слева на навигационную панель: одна — возвратная
[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 делегата и сделать себя делегатом:
Для того, чтобы на приборах с Compact-шириной (все iPhones и iPhone 6+ и iPhone 6s+ в портретном режиме) при старте (когда imageURL = nil) появлялся Master, а не Detail (как это принято в SplitViewController) реализуем следующий метод collapseSecondaryViewController:ontoPrimaryViewController: делегата UISplitViewControllerDelegate
Этот метод делегата спрашивает нас, хотим ли мы при переходе в 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+ при переходе из портретного режима в ландшафтный при условии, чтобы мы находимся в списке фотографий. Мы должны получить следующую последовательность экранов
Но мы ее не получим, а получим при переходе из портретного режима в ландшафтный режим другую неправильную последовательность экранов, так как Split View Controller вместе с segue типа «ShowDetail» хватает» текущий View Controller (если это не Master) и превращает его в Detail.
В нашей ситуации primaryViewController — это Navigation Controller для Master, а его топовый View Controller — это список фотографий PlaceFlickrPhotos. Вначале мы фиксируем эту особую ситуацию и получаем список фотографий masterView.
Но мы не хотим, чтобы masterView вернулся в качестве Detail, мы хотим вернуть ImageViewController (изображение фотографии), «завернутый» в Navigation Controller для Detail. Но нам совсем неоткуда взять Navigation Controller для Detail , даже параметр splitViewController, указанный в методе делегата, нам не поможет, так как в данный момент он содержит всего один ViewController и это Navigation Controller для Master.
Остается взять Navigation Controller для Detail со storyboard, но это можно сделать, если задать идентификатор detailNavigation на storyboard для Navigation Controller для Detail :
Ниже представлен код:
. . . . . . . . . . . . . . . . . . . .
Кроме того, мы выполняем определенную настройку Detail: добавляем кнопки на навигационную панель и выставляем фотографию, указанную первой в списке фотографий.
Вот полный код метода separateSecondaryViewControllerFromPrimaryViewController делегата UISplitViewControllerDelegate:
Теперь UISplitViewController функционирует правильно на любых устройствах, включая iPhone 6+ и iPnone 6s+.
Добавляем Tab Bar Controller вместе с другой закладкой Resents
Пробуем запустить приложение и обнаруживаем, что на iPad все работает нормально, а на всех iPhone в Compact—width режиме ImageViewController представляется на экране модально с заголовком, но без навигационной панели и без дополнительных кнопок на ней и мы не можем никуда с этого экрана двинуться:
Это говорит о том, что SplitViewController не может поместить наш ImageViewController в стэк Navigation Controller для Master, так как SplitViewController управляет Master через Tab Bar Controller, который и предлагает модальное представление Detail. Чтобы исправить эту ситуацию, мы можем сами поместить ImageViewController в стэк Navigation Controller для Master и будем это делать в методе showDetailViewController: sender: делегата UISplitViewControllerDelegate для Compact—width режима.
Сначала определяем Compact—width режим и получаем Navigation Controller для Master:
. . . . . . . . . . . . . . . . . . . . . . . . .
затем извлекаем ImageViewController и помещаем его в стэк Navigation Controller для Master и показываем его на экране:
. . . . . . . . . . . . . . . . . . . . . . . . . .
Возврат YES говорит о том, что SplitViewController не должен управлять показом Detail, мы сами будем его показывать. Теперь изображение фотографии для Compact—width режима будет показываться правильно: c возвратной кнопкой.
Но коль скоро мы добавили ImageViewController в стэк Navigation Controller для Master, то можем получить наш ImageViewController в Master при переходе в ландшафтный режим:
При переходе в ландшафтный режим нам нужно убрать ImageViewController из стэка Navigation Controller для Master:
Пользуясь знанием imageURL только что убранного ImageViewController, можем настроить соответствующую строчку в списке фотографий:
. . . . . . . . . . . . . . . . . . . . . . .
В результате ситуация на iPhones поправится, включая и переход от портретного режима к ландшафтному на iPhone 6+:
Теперь все работает правильно на всех приборах.
Пункт 11
Список “недавних” фотографий должен сохраняться в NSUserDefaults (то есть они должны постоянно сохранятся между запусками данного приложения). Будет более удобно, если массивы, которые возвращаются с Flickr, все были бы property lists (сразу же после преобразования из формата JSON).
Создаем новый класс RecentsUserDefaults, в котором будет два метода класса: один для сохранения массива фотографий в NSUserDefaults, а другой — для извлечения массива фотографий из NSUserDefaults:
Запоминать только что просмотренную фотографию будем в методе prepareImageViewController: toDisplayPhoto: forRow: класса FlickrPhotos:
Для показа «недавних» фотографий создадим класс ResentsTVC, который наследует от класса FilckrPhotos и единственной работой которого будет загрузка фотографий из NSUserDefaults:
Таким образом мы использовали подсказку № 9:
Используйте объектно-ориентированное программирование для инкапсуляции функциональности в вашем приложении. Допустим, что в вашем приложении вам понадобится 5 различных UITableViewController subclasses. Вы могли бы подходящим образом “разделить” (share) код между ними, и получить прекрасно сконструированные, повторно используемые Controller с понятными API и Model. И это совершенно правильно: вначале создать UITableViewController subclass, который будет что-то делать, а затем создать subclass вновь полученного класса для того, чтобы делать уже что-то более изысканное.
Пользовательский класс ResentsTVC устанавливаем на storyboard Resents:
Запускаем приложение, выбираем закладку Resents, получаем список «недавно» просмотренных фотографий и можем получить их изображение:
Но для того, чтобы наша адаптивном приложение работало бы для ResentsTVC, нам нужно внести некоторые совсем небольшие изменения в метод separateSecondaryViewControllerFromPrimaryViewController делегата UISplitViewControllerDelegate. Для того, чтобы этот код выполнялся для любого класса, который наследует от класса FlickrPhotos, мы должны заменить наш конкретный класс PlaceFlickrPhotos на базовый класс FlickrPhotos:
. . . . . . . . . . . . . . . . . . .
Теперь этот код выполняется для любого класса, наследующего от класса FlickrPhotos (класс PlaceFlickrPhotos и ResentsTVC ).
Продолжая использование объектно-ориентированного программирования для инкапсуляции функциональности в нашем приложении, высказанные в подсказке №9 (текст приведен выше), давайте сделаем класс TopPlacesTVC более обобщенным классом (наподобие FlickrPhotos), отображающим топовые места Flickr независимо от того, как они были получены (например из NSUserDefaults), а само получение топовых мест из сети перенесем в класс CurrentTopPlacesTVC.
В классе TopPlacesTVC останется public API в виде массива топовых мест;
а в класс CurrentTopPlacesTVC перенесем получение топовых мест с сервера Flickr.com
Обратите внимание на то, что метод fetchTopPlaces превращен в IBAction простой заменой void на IBAction. Это в дальнейшем нам поможет практически бесплатно получить Refresh Control. Для этого идем на storyboard и включаем Refresh Control.
Затем с помощью CTRL-перетягивания подключаем Refresh Control к коду:
Осталось добавить старт и остановку Refresh Control, при этом остановка Refresh Control потребует вернуться в main queue:
Не забудьте поменять на storyboard пользовательский класс на CurrentTopPlacesTVC:
Теперь работает Refresh Control:
То же самое нужно сделать и со списком фотографий PlaceFlickrPhotos:
Но здесь нам придется учесть замечание № 16:
Оказывается, что UIRefreshControl не всегда появляется, если вы вызываете beginRefreshing программным путем. Если вы бесстрашны, то можете это доработать заставив UITableView подкручиваться вверх путем установки его contentOffset (помните, что UITableView является UIScrollView) отрицательного значения координате y (установка ее равной высотеheight refreshControlбыло бы лучшим вариантом). Однако эта доработка не является Обязательным заданием (старт refreshControl все равно происходит, даже если его не подкрутить чуть-чуть наверх, чтобы он появился).
В код необходимо добавить одну строку:
Теперь все работает. Код на Github.
Пункт 12
Ваше приложение должно работать на реальном устройстве.