Адаптивные SplitViewController и Popover для iOS 9. (Swift). Часть 1

Screen Shot 2015-12-02 at 9.32.13 PM
Screen Shot 2015-12-02 at 9.35.24 PM
Проводя адаптацию предыдущего стэнфордского курса «Developing iOS  7 Apps for iPhone and iPad» 2014 г. к iOS 9, я обнаружила, что в курсе «Developing iOS 8 Apps with Swift» 2015 г. практически отсутствует настройка адаптивного Split View Controller, так как там он использовался только для Графического Калькулятора на Лекции 6 и 7 и в Домашнем Задании 3. В этом варианте Master не представлял собой традиционную таблицу или каскад таблиц, выбирая из которых соответствующий элемент, мы получали в качестве Detail изображение, связанное с выбранным элементом.

В курсе по iOS 8 нам не требовалось более скрупулезной настройки адаптивного UI, состоящего из SplitViewController и Popover.
Мне представляется целесообразным восполнить этот пробел и реализовать универсальное приложение Photomania Universal URL, которое представлено в стэнфордском курсе  «Developing iOS  7 Apps for iPhone and iPad» 2014 г, на Swift в iOS 9.  Приложение Photomania на Swift и iOS 9 находится на  Github. Давайте подробно рассмотрим как оно было построено.

При проектировании этого приложения на iOS 9 и Swift мы должны решить три вопроса:

  1. создать адаптивный интерфейс в iOS 9 на основе концепции Size Classes
  2. обеспечить сохранение данных в Core Data и работу с ними UI приложения
  3. осуществлять подачку данных с сервера Flickr в фоновом режиме

В этом посте решается первая задача, а именно, — создание адаптивного интерфейса со  SplitViewController и Popover.

Что представляет собой приложение  Photomania Universal URLПриложение Photomania Universal URL запрашивает сервер Flickr о наиболее “свежих” по времени фотографиях, составляет список фотографов, сделавших эти фотографии, и показывает  его вам. Eсли вы кликните на фотографе, то в таблице появляется список сделанных им фотографий. Затем вы можете кликнуть на любой фотографии в списке, и получить как полномасштабное изображение вашей фотографии, так и URL этого изображения.

Вот как работает это приложение с iPhone:
Screen Shot 2016-03-14 at 11.27.46 AM

А вот как оно работает с iPad:
Screen Shot 2016-03-14 at 11.19.39 AM

В фоновом режиме (in the background) приложение постоянно запрашивать Flickr о все большем и большем количестве последних фотографий и забрасывать их в базу данных Core Data. Список фотографов на экране отслеживает появление этих данных, которые постоянно автоматически обновляются.
Однако спроектированное таким образом для iOS 7 приложение Photomania Universal URL не будет правильно работать на всех приборах в iOS 9: на iPhone 5s и более старших моделях iPhones снизу и сверху на экране будут появляться черные полосы.
Поэтому была поставлена задача создать современный адаптивный UI этого приложения для iOS 9 на Swift.
 Вот с каким приложением мы собираемся иметь дело. Приложение было спроектировано для iOS 7 и написано на Objective-C. Реализация его включала в себя две storyboards: одна — для iPhone, а другая — для iPad (приведены ниже). Это было в порядке вещей, и ни о каком адаптивном интерфейсе, кроме как использования механизма Autolayout, речь не шла.
С незапамятных времен Split View Controller и Popover в iOS были доступны только на iPad. Начиная с iOS 8, они теперь работают и на iPad, и на iPhone, благодаря концепции Size Classes и их адаптивному поведению. Однако автоматическая адаптация, предложенная Apple «из коробки», чаще всего нас не устраивает и приходится писать небольшой дополнительный код, используя методы делегатов UISplitViewControllerDelegate и UIPopoverPresentationControllerDelegate. В данной статье мы будем исследовать адаптивные способности Split View Controller и Popover на примере очень простых практических приложений, работающих с сервером Flickr.com, представляющим собой облачный сервис для хранения фотографий. Сама по себе эта задача имеет большой практический смысл, так как является часто встречающимся случаем, когда данные считываются с некоторого сервера и представляются затем ввиде связанных таблиц и изображений. Попутно мы будем демонстрировать “вживую” такие синтаксические конструкции Swift, как вычисляемые свойства c {get} и {set}, наблюдатели свойств didSet{}, функции высшего порядка map, flatMap, filter, вывод типа из контекста и перегрузку (overload) функций, совместное использование Swift и Objective-C кода, работу со структурами struct, использование хранилища NSUserDefaults и т.д. Но все же в этой статье акцент делается на более сложных конфигурациях адаптивных Split View Controller и Popover.
Впоследствие все приведенные в этой статье приложения вы сможете использовать в качестве шаблонов для разработки ваших приложений с похожими задачами
.
Итак, согласно новой философии iOS, пользовательский интерфейс (UI) может достаточно быстро настраиваться на любой тип прибора в зависимости от того, какой Size Class имеет его экран.
Для классификации различных приборов в iOS 8 было введено понятие Size Class. Всего четыре Size Classes:

  • Horizontal Regular
  • Horizontal Compact
  • Vertical Regular
  • Vertical Compact

Ваш View Controller ( Split View Controller или Popover ) всегда существует в среде Size Сlass с определенной шириной (width) и высотой (height). В настоящий момент Size Class может быть либо Compact, либо Regular:
Screen Shot 2015-11-20 at 10.53.24 PM
Полноразмерные View Controllers на iPad всегда являются Regular по обоим направлениям (горизонтальному и вертикальному). На всех iPhones перед появлением iPhone 6+ и iPhone 6s+, горизонтальный размер всегда был Compact (и в портретном, и в ландшафтном режимах), а вертикальный размер был Regular в портретном режиме и Compact в ландшафтном режиме. С появлением больших iPhones 6+ и iPhones 6s+ у них, в отличие от других iPhones, ширина стала Regular в ландшафтном режиме, что и дало основание распространить применение полноценного Split View Controller на iPhones 6+ и iPhones 6s+ в ландшафтном режиме.
Будет не очень удобно, если я для своих приложений буду создавать различные интерфейсы для всех 4-х ситуаций. Поэтому в Xcode 7 у нас есть еще один Size Class, называемый Any (wAny hAny), который мы используем при проектировании универсальных приложений:

Screen Shot 2015-11-22 at 8.14.50 PM
В iOS 7 нам приходилось при создании универсального приложения использовать два совершенно разных пользовательских интерфейса, построенных на разных типах Controllers (ниже представлены storyboards приложения Photomania Universal URL из предыдущего стэнфордского курса CS193P Осень 2013 — Зима 2014г. “Developing iOS 7 Apps for iPhone and iPad”):
Split View Controller на iPad
Main_iPad.storyboard
Screen Shot 2015-11-19 at 9.29.11 PM
Navigation Controller на iPhone
Main_iPhone.storyboard
Screen Shot 2015-11-19 at 9.48.33 PM
В iOS 8 Split View Controller становится адаптивным. Это означает, что единственный Split View Controller может управлять двумя этими архитектурами для двух типов приборов.
Для получения адаптивного интерфейса нужно выбрать файл storyboard с Split View Controller, указать в Инспекторе Файла режим Size Class и Auto Layout
Screen Shot 2015-11-21 at 3.08.55 PM
и добавить новый адаптивный segue типа Show Detail:
Screen Shot 2015-11-21 at 5.03.35 PM
Адаптивная storyboard очень похожа на storyboard для iPad в iOS 7, за исключением двух вещей:

  1. размеры всех экранных фрагментов одинаковые и соответствуют универсальному в Xcode классу wAny hAny,
  2. появился новый адаптивный segue типа Show Detail Segue, который помогает Split View Controller осуществлять сразу две функции: “скольжение”, как в Navigation Controller для iPhones, и «переезд» к Detail, как это принято в классическом Split View Controller для iPads.

Далее нам предстоит исследовать работу адаптивного Split View Controller на различных приборах, и в случае отклонения его работы от привычного нам функционирования вносить определенные дополнения в код. Количество этих дополнений зависит от того, какие элементы входят в Master и Detail. Мы перечислим пять интересных с точки зрения разработчика случаев, которые отличаются сложностью Master, а Detail везде один и тот же — единственный ImageViewController, вставленный в Navigation Controller и призванный показывать изображение фотографии:

1. Классический вариант: один элемент в Master, вставленный в Navigation Controller, (часто это Table View Controller) — приложение AdaptiveSplitViewController1Swift на Github.

Screen Shot 2016-03-14 at 3.24.45 PM

2. Множество Table View Controller элементов, вставленных в Navigation Controller — приложение AdaptiveSplitViewController2Swift на Github.
Screen Shot 2016-03-14 at 3.29.53 PM

3. Tab Bar Controller в качестве Master — приложение AdaptiveSplitViewController3Swift на Github.
Screen Shot 2016-03-14 at 3.32.35 PM

4. Случай разных UI и разных пользовательских классов для приборов с разными Size Classes здесь не рассматривается, но идею можно посмотреть в “Адаптивный интерфейс с двумя storyboards для iOS 9”.

5. Адаптивный Popover — приложение AdaptiveSplitViewController4Swift на Github.
Screen Shot 2016-03-14 at 3.35.07 PM

Проще всего понять необходимость дополнительных поправок, если экспериментировать не с громоздким приложения Photomania Universal URL c “подкачкой” Flickr фотографий в фоновом режиме в Core Data, а с очень простым приложением, также работающим с фотографиями, находящимися на сервере Flickr. Информация об этих фотографиях асинхронно считывается в приложении и размещается в таблице с тем, чтобы после выбора пользователем определенной фотографии, показать ее полномасштабное изображение. Проектирование такого приложения с “чистого листа” позволит нам “убить двух зайцев”. С одной стороны, продемонстрировать синтаксические конструкции Swift. А, с другой стороны, использовать это простое экспериментальное приложение в качестве базового для более сложных конфигураций адаптивных Split View Controller и Popover.

Экспериментальное приложение

Открываем новый проект для универсального приложения на Swift, называем его AdaptiveSplitViewController1Swift и используем шаблон Single View Application (именно этот шаблон, а не Master-Detail Application, который мы будем исследовать позже).

Screen Shot 2016-03-14 at 3.38.01 PM
Будем проектировать наше приложение с «чистого листа». Поэтому уберем со storyboard единственный экранный фрагмент View Controller и оставим storyboard совершенно пустой. Удалим также файл ViewController.swift.
Установим на storyboard адаптивный режим, то есть включим опции Size Class и Auto Layout:
Screen Shot 2016-03-14 at 6.28.34 PM
Перетянем из палитры объектов на storyboard Table View Controller для списка Flickr фотографий и обычный View Controller для полномасштабного изображения фотографии. Последний экранный фрагмент получит URL фотографии в качестве Модели и покажет ее изображение. Поэтому он будет называться Image View Controller.
Screen Shot 2016-03-14 at 6.16.23 PM
и соединим между собой Table View Controller и Image View Controller обычным Show segue c идентификатором Show Photo:
Screen Shot 2016-03-14 at 6.35.50 PM
Устанавливаем Navigation Controller в качестве стартового экранного фрагмента:
Screen Shot 2016-03-14 at 6.48.16 PM
В Table View Controller устанавливаем стиль прототипа динамической ячейки как Subtitle и задаем идентификатор photoCell для повторно используемой ячейки
Screen Shot 2016-03-14 at 6.54.22 PM
Для таблицы с фотографиями создаем очень простой класс FlickrPhotosTVC, который наследует от UITableViewController и задача которого состоит в показе списка фотографий, представленных массивом структур Photo:
FlickrPhotosTVC.swif
Screen Shot 2016-03-14 at 6.59.16 PM
Структура Photo, которую мы разместим в файле DataModel.swift, содержит заголовок фотографии title, более детальную информацию о фотографии subtitle, уникальный идентификатор фотографии unique, URL изображения imageURL, сделавшего фотографию фотографа photographer
DataModel.swift

Screen Shot 2016-03-14 at 7.04.07 PM
Структура struct Photo имеет convenience инициализатор init?(json:[String:AnyObject]), у которого на входе JSON данныe, полученные с сервера Flickr в виде словаря [String:AnyObject]. Это Optional инициализатор, и именно поэтому у него стоит знак ?вопроса в имени init?:
DataModel.swift
Screen Shot 2016-03-14 at 7.49.30 PM
Есть специальные требования формирования атрибутов Photo. Если у фотографии нет заголовка title, то нужно использовать детальное описание subtitle фотографии как title. Если у фотографии нет ни заголовка title, ни детального описания subtitle, нужно использовать “Unknown” как title. Ключи FLICKR_PHOTO_TITLE, FLICKR_PHOTO_ID, FLICKR_PHOTO_DESCRIPTION и FLICKR_PHOTO_OWNER словаря с информацией о Flickr фотографии определены в файле FlickrFetcher.h, содержащем public API для Flickr (об этом немного позже).
Массив структур var photos = [Photo] является Моделью для MVC FlickrPhotosTVC и используется в методах UITableViewDataSource:
FlickrPhotosTVC.swift
Screen Shot 2016-03-14 at 7.53.38 PM
А также при “переезде” на Image View Controller в предположении, что Моделью обслуживающего его пользовательского класса ImageViewController является свойство var imageURL: NSURL?, задающее URL изображения фотографии. Делаем подготовку нового segue типа Show Detail в коде класса FlickrPhotosTVC таким образом, чтобы учесть присутствие Navigation Controller, предшествующего Image View Controller, и добавить имеющуюся у Split View Controller кнопку displayModeButtonItem на навигационную панель (но об этом позже):
FlickrPhotosTVC.swift
Screen Shot 2016-03-14 at 8.03.00 PM
Константы для storyboard собраны в privater структуре Storyboard:
FlickrPhotosTVC.swift
Screen Shot 2016-03-14 at 8.05.53 PM
Cогласно концепции объектно-ориентированного программирования сделаем класс FlickrPhotosTVC более обощенным (generic), то есть он будет формировать таблицу фотографий, представленных массивом [Photo] и при выборе определенной строки в таблице показывать изображение соответствующей фотографии с помощью MVC ImageViewController. Но он не будет заботится о том, как получен массив [Photo]: c сервера Flickr или из хранилища NSUserDefaults.
Поэтому для показа фотографий c сервера Flickr мы создаем новый класс JustPostedFlickrPhotosTVC, который является subclass класса FlickrPhotosTVC:
JustPostedFlickrPhotosTVC.swift
Screen Shot 2016-03-14 at 8.08.49 PM
Данные c сервера Flickr будут загружаться в классе JustPostedFlickrPhotosTVC c использованием public API для Flickr, который был предоставлен стэнфордским сайтом в виде папки Flickr Fetcher. Он позволяет получить URLs для различных запросов к Flickr, код его написан на Objective-C. Наиболее простой способ включить Objective-C код в Swift проект это копировать по одному файлу из папки Flickr Fetcher (их там всего три) в ваш Swift проект, тогда перед тем, как скопировать файл FlickrFetcher.m вас спросят, хотите ли вы добавить Objective-C файл заголовка для связи со Swift?:
Screen Shot 2016-03-14 at 8.14.45 PM
Вы отвечаете “Создать связующий файл заголовка”. В результате сформируется пустой файл заголовка AdaptiveSplitViewController1Swift-Bridging-Header.h для вашего проекта, в который вы добавляете необходимые файлы заголовков public API для Flickr:
Screen Shot 2016-03-14 at 8.16.38 PM
Все. Вам больше ничего не надо делать, теперь вы можете обращаться к классам и константам public API для Flickr напрямую. Вам останется только получить ключ Flickr API. Бесплатный Flickr.com account вполне подойдет, так как вы не будете размещать на Flickr фотографии, а только запрашивать их.
Вновь полученный файл заголовка автоматически пропишется в настройках проекта:
Screen Shot 2016-03-14 at 8.19.42 PM
Вернемся к классу JustPostedFlickrPhotosTVC, который специально предназначен для считывания наиболее “свежих” фотографии с Flickr, и используем public API для формированию URL запроса о “недавних” фотографиях. Считывание данных по этому URL происходит асинхронно и полученная информация преобразуется в массив [Photo]:
JustPostedFlickrPhotosTVC.swift
Screen Shot 2016-03-14 at 8.23.21 PM
Преобразование JSON данных в массив структур [Photo] осуществляется одной строкой благодаря функции flatMap, выводу типа из контекста и Optional инициализатору Photo.
Полученный в результате парсинга массив self.photos “подхватывается” superclass FlickrPhotosTVC и мы видим таблицу фотографий при условии, что выставили на storyboard пользовательский класс для таблицы Flickr Photos как JustPostedFlickrPhotosTVC:
Screen Shot 2016-03-14 at 8.28.26 PM
Для того, чтобы показать пользователю, что идет загрузка данных из сети, которая требует некоторого времени, включим на storyboard Refresh Control для экранного фрагмента Flickr Photos:
Screen Shot 2016-03-15 at 12.35.10 PM
Перед методом func fetchPhotos () загрузки данных с сервера Flickr поставим @IBAction — то есть покажем, что это Action:
JustPostedFlickrPhotosTVC.swift
Screen Shot 2016-03-15 at 12.38.20 PM
C помощью CTRL-перетягивания привяжем Refresh Control к @IBAction func fetchPhotos ():
Screen Shot 2016-03-15 at 12.40.24 PM
Для получения полномасштабного изображения фотографии используется класс ImageViewController, Моделью которого является var imageURL: NSURL?:
ImageViewController.swift
Screen Shot 2016-03-15 at 12.42.12 PM
Подробно проектирование этого класса описано в Лекции 9 CS193P Winter 2015 — Scroll View и Многопоточность (Multithreading) курса «Developing iOS 8 Apps with Swift» стэнфордского университета CS193P зима 2015 года.
Устанавливаем этот класс оставшемуся на storyboard Image View Controller:
Screen Shot 2016-03-15 at 12.44.59 PM
Запускаем приложение, ждем, когда закончится загрузка c Flickr, выбираем фотографию, получаем ее изображение.
Screen Shot 2016-03-15 at 12.46.41 PM
Все функционирует правильно и хорошо выглядит на Compact-width приборах (все iPhones, кроме iPhone 6+ и iPhone 6s+, для которых этот режим справедлив только в портретном виде), но на Regular-width приборах (все iPad и iPhone 6+ и iPhone 6s+ в ландшафтном режиме) следует использовать Split View Controller.
Код находится на Github — приложение AdaptiveSplitViewController1Swift.
На этом мы закончим формирование нашего экспериментальное приложения и приступим к реализации адаптивного Split View Controller в классическом варианте.

1. Классический вариант адаптивного Split View Controller

Для этого создадим еще одну storyboard в этом приложении с помощью меню File -> New -> File -> User Interface:
Screen Shot 2016-03-15 at 12.49.35 PM
Назовем ее условно iPad.storyboard (на самом деле имя не имеет никакого значения, так как впоследствии мы переименуем iPad.storyboard в Main.storyboard):
Screen Shot 2016-03-15 at 1.22.32 PM
В результате получим пустую iPad.storyboard. Для того, чтобы приложение работало с iPad.storyboard добавим код в AppDelegate.swift:
Screen Shot 2016-03-15 at 1.24.25 PM
Вытяним из Палитры Объектов и разместим на storyboard Split View Controller, который появляется вместе с двумя сопровождающими View Controllers, которые мы тут же удалим, а вместо Master копируем с Main.storyboard Flickr Photos, вставленный в Navigation Controller, и вместо Detail — Image View Controller, также вставленный в Navigation Controller.
Screen Shot 2016-03-15 at 1.28.29 PM
Используем новый адаптивный segue типа Show Detail с идентификатором Show Photo.
Screen Shot 2016-03-15 at 1.30.29 PM
В скобках на картинке в Инспекторе Атрибутов дается уточнение, что segue типа Show Detail — это Replace segue, а это означает, что Detail этого Split View Controller будет замещаться новым экземпляром MVC. Это очень важно для дальнейшего понимания. Как и всякий другой seguesegue типа Show Detail нуждается в подготовке, особенно в случае, если destination ( тот View Controller, куда мы “переезжаем” благодаря этому segue) — это UINavigationController:
FlickrPhotosTVC.swift
Screen Shot 2016-03-15 at 1.33.52 PM
Следует обратить внимание, что при использовании segue происходит полная замена Detail новым MVC, а prepareForSegue работает еще до загрузки ImageViewController и установка нового значения Модели (то есть imageURL) может не обновить до конца пользовательский интерфейс Image View Controller, так как некоторые outlets еще не выставлены и мы еще не находимся полностью на экране. Поэтому класс ImageViewController спроектирован таким образом, что при установке Модели — imageURL, выборка данных об изображении фотографии с сервера Flickr и обновление пользовательского интерфейса происходит только, если ImageViewController уже находится на экране:
ImageViewController.swift
Screen Shot 2016-03-15 at 1.40.59 PM
В нашем случае, когда Detail будет полностью замещается новым экземпляром MVC, выборку данных из сети и обновление UI реализуем в методе “жизненного цикла” viewWillAppear, то есть непосредственно перед появлением изображения image на экране:
ImageViewController.swift
Screen Shot 2016-03-15 at 2.05.15 PM
Screen Shot 2016-03-15 at 2.07.01 PM
Запускаем приложение на iPhone 6+ и на iPad в портретном режиме:
Screen Shot 2016-03-15 at 2.09.50 PM
На iPhone 6+ и iPad 2 появляется пустой экран — это Detail (в нашем случае ImageViewController) для SplitViewController, и это было обычное поведение SplitViewController ранее, когда он проектировался только для iPad. Но если на iPhone 6+ есть возвратная кнопка к Master (в нашем случае это таблица со списком Flickr фотографий), то на iPad даже непонятно, что делать. Пользователь должен каким-то магическим способом догадаться, что работает жест swipe, который покажет нам Master, то есть список Flickr фотографий.
Дальше можно выбирать фотографию и экран с фотографией будет автоматически обновляться. Все работает, но нет возвратной кнопки в Master.
Запускаем приложение на iPad в портретном режиме.
Screen Shot 2016-03-15 at 8.09.09 PM
Запускаем приложение на iPad в ландшафтном режиме.
Screen Shot 2016-03-15 at 8.11.44 PM
Все работает.
Запускаем приложение на iPhone 6+ в портретном режиме
Screen Shot 2016-03-15 at 8.13.35 PM
При запуске в портретном режиме есть возвратная кнопка, призывающая нас выбрать Flickr фотографию. Нажимаем на эту кнопку, и действительно попадаем в Master, то есть экранный фрагмент со списком фотографий. Дальше можно выбирать фотографию и получать ее изображение. Этот режим работает.
Переходим в ландшафтный режим на iPhone 6+
Screen Shot 2016-03-15 at 8.16.48 PM
Все работает. Фотографии обновляются в Detail.
Какой можно сделать вывод из этих экспериментов?
Если мы имеем дело с Regular-width приборами (iPad в портретном и ландшафтном режимах и iPhone 6+, iPhone 6s+ в ландшафтном режиме), то на экране одновременно находятся и Master, и Detail. Этот режим для адаптивного Split View Controller назван expanded. Это известный нам ранее привычный режим обычного Split View Controller.
Если мы имеем дело с Compact-width приборами ( iPhone 6+, iPhone 6s+ в портретном режиме, все другие iPhones в портретном и ландшафтном режимах), то на экране находится только один MVC: либо Master, либо Detail, поэтому этот режим назван collapsed для адаптивного Split View Controller. Это режим, когда адаптивного Split View Controller работает как Navigation Controller, в стэке которого находятся и Master, и Detail.
У нас одна storyboard, которая работает на обоих платформах (iPhone и iPad) и автоматически адаптируется.
Но нас не устраивают две вещи:

  • Мы хотим, чтобы при старте на любом iPad в портретном режиме появлялась такая же возвратная кнопка, как и при старте на любом iPhone в портретном режиме;
  • При старте на любом iPhone в портретном режиме появлялся бы не Detail с пустым экраном (как это было принято для iPad), а Master (как это было принято для iPhone).

Для того, чтобы при старте на любом iPad в портретном режиме появлялась возвратная кнопка необходимо разместить ее явно на навигационной панели в AppDelegate.swift:
Screen Shot 2016-03-16 at 9.51.16 AM
и в методе prepareForSegue в классе FlickrPhotos:
FlickrPhotos.swift
Screen Shot 2016-03-16 at 9.54.57 AM
Кроме этого дать заголовок Navigation Controller для Master, который в нашем случае будет “Flickr Photos”:
Screen Shot 2016-03-16 at 10.12.58 AM
В результате мы имеем необходимую возвратную кнопку для iPad в портретном режиме и кнопку переключения режимов для iPhone 6+, iPhone 6s+ в ландшафтном режиме.
Screen Shot 2016-03-16 at 10.14.54 AM
Вот как работает кнопка переключения режимов на iPhone 6+, iPhone 6s+ в ландшафтном режиме:
Screen Shot 2016-03-16 at 10.16.19 AM
Теперь работу адаптивного Split View Controller для Regular-width приборов можно считать удовлетворительной.
Что нас не устраивает в работе адаптивного Split View Controller для Compact-width приборов?
На iPhones работа должна сразу начинаться с показа Master и дальше двигаться последовательно в сторону Detail c помощью стэкового механизма Navigation Controller. Это обеспечивается методами делегата UISplitViewControllerDelegate, один из которых мы сейчас реализуем в AppDelegate.
Вначале мы подтверждаем протокол UISplitViewControllerDelegate:
AppDelegate.swift
Screen Shot 2016-03-16 at 10.20.21 AM
И, наконец, реализуем метод collapseSecondaryViewController, ontoPrimaryViewController делегата UISplitViewControllerDelegate, который срабатывает при переходе в collapsed режим, когда на экране должен остаться только один View Controller. Он спрашивает нас, нужно ли отбросить Detail? Если мы отвечаем true, то на экране в collapsed режиме остается только Master, если false — то Detail. Мы хотим иметь Master только при старте, то есть когда Detail — это Navigation Controller, его топовым View Controller в стэке является ImageViewController , Модель которого imageURL имеет значение равное nil. Именно это условие мы выделяем прежде, чем возвратить true:
AppDelegate.swift
Screen Shot 2016-03-16 at 10.28.16 AM
Стартуем iPhone 6+ в портретном режиме:
Screen Shot 2016-03-16 at 10.32.04 AM
Теперь наш адаптивный интерфейс работает как нужно, то есть для Compact-width приборов мы стартуем со списка Flickr фотографий.
Итак, настройка адаптивного Split View Controller для классического случая завершена.
Переименуем storyboard Main.storyboard в iPhone.storyboard и оставим ее для истории в этом приложении, а iPad.storyboard в Main.storyboard и сделаем ее основной для универсального приложения:
Screen Shot 2016-03-16 at 10.34.05 AM
, а также избавимся от лишнего кода в AppDelegate:
Screen Shot 2016-03-16 at 10.36.31 AM
Окончательный вариант простейшего приложения AdaptiveSplitViewController1Swift находится на Github.

Подведем итог нашим действиям по созданию адаптивного Split View Controller:

Шаг 1. Перетаскивая Split View Controller из Палитры объектов.
Шаг 2. Вставляем Master и Detail в Navigation Controller и подсоединяем к Split View Controller.
Шаг 3. Добавляем segue типа Show Detail
Шаг 4. Настраиваем для segue метод prepareForSegue с учетом того, что destinationViewController для этого segue может быть как ImageViewController, так и UINavigationController, в стэке которого на самом верху находится ImageViewController.
Шаг 5. Добавляем кнопки на навигационную панель в методе prepareForSegue подготовки segue и в AppDelegate.
Шаг 6. Реализуем в AppDelegate метод collapseSecondaryViewController:ontoPrimaryViewController: делегата UISplitViewControllerDelegate, который срабатывает при переходе в collapsed режим, когда на экране должен остаться только один View Controller; в этом случае мы отбрасываем Detail при старте.

Можно обойтись без этих 6 шагов и получить точно такой же код и UI сразу же, если воспользоваться прекрасным Master-Detail шаблоном приложения, в котором уже есть и Show Detail segue, и Navigation Controller, и весь необходимый код.
Screen Shot 2016-03-16 at 10.39.48 AM
Screen Shot 2016-03-16 at 10.41.07 AM
Мы можем еще немного улучшить работу Split View Controller для iPad, задав преимущественный режим показа .AllVisible:
AppDelegate.swift
Screen Shot 2016-03-16 at 10.43.11 AM
Что означает preferredDisplayMode? Это свойство определяет предпочтительный режим показа. Split View Controller делает все возможное, чтобы настроить интерфейс соответствующим образом, но может использовать и другой режим показа, если, например, будет недостаточно места для показа в заданном вами предпочтительном режиме. По умолчанию значение свойства preferredDisplayMode равно .Automatic. На iPad это приводит к использованию режима .PrimaryOverlay в портретной ориентации и .AllVisible в ландшафтной ориентации. Задавая предпочтительный режим показа .AllVisible, мы, фактически, влияем только на показ Split View Controller на iPad в портретном режиме
Screen Shot 2016-03-16 at 10.44.55 AM
В AppDelegate.swift приведены еще две закомментированные строки кода
// splitViewController.preferredPrimaryColumnWidthFraction = 0.5
// splitViewController.maximumPrimaryColumnWidth = 512

Если вы снимите комментарии с этих строк, то сможете регулировать ширину столбцов для Master и Detail. Вы можете использовать свойство preferredPrimaryColumnWidthFraction, которое принимает значение от 0.0 до 1.0, чтобы представить долю от общей ширины экрана, которую занимает Master. По умолчанию это свойство принимает значение UISplitViewControllerAutomaticDimension, которое приводит к подходящей ширине Master, выбираемой Split View Controller.
Действительная ширина Master ограничивается значениями в диапазоне minimumPrimaryColumnWidth и maximumPrimaryColumnWidthSplit View Controller будет делает все возможное, чтобы настроить интерфейс соответственно заданным вами значениям свойств, но может поменять их на другие в зависимости от располагаемого пространства. Вы можете получить действительную ширину Master с помощью свойства primaryColumnWidth.
Screen Shot 2016-03-16 at 10.47.29 AM
Код находится на Github — приложение AdaptiveSplitViewController1Swift.

2. Множество Table View Controllers в Master

Но пойдем дальше и добавим еще один экранный фрагмент Table View Controller для фотографов, которые сделали Flickr фотографии.
Для этого на основе предыдущего приложения AdaptiveSplitViewController1Swift создадим новое приложение AdaptiveSplitViewController2Swift и включим в Master еще одну Table View Controller. Теперь наш пользовательский интерфейс выглядит следующим образом:
Screen Shot 2016-03-14 at 3.29.53 PM
Вначале мы выбираем фотографа, потом любую фотографию этого фотографа из списка его фотографий, а затем показываем выбранную фотографию:
Screen Shot 2016-03-16 at 11.58.36 AM
Экранный фрагмент Table View Controller для фотографов обслуживает достаточно простой класс FlickrPhotographersTVC. Это практически копия класса FlickrPhotoTVС, там тоже есть свойство var photos = [Photo](), представляющее собой список фотографий, но его установка приводит к вычислению другого свойства var photographers = [Photographer](), которое представляет собой список фотографов, сделавших эти фотографии, и является Моделью для класса FlickrPhotographersTVC:
FlickrPhotographersTVС.swift
Screen Shot 2016-03-16 at 12.01.08 PM
Фотография Photo и фотограф Photographer задаются структурами и находятся в файле DataModel.swift:
Screen Shot 2016-03-16 at 12.02.40 PM
На основе Модели var photographers = [Photographer]() реализованы методы UITableViewDataSource:
FlickrPhotographersTVС.swift
Screen Shot 2016-03-16 at 12.04.59 PM
Для подготовки “переезда” на таблицу со списком фотографий Flickr Photos используется метод prepareForSegue, который также как и в классе FlickrPhotos учитывает, что destinationViewController, на который мы “переезжаем”, может быть вставлен в Navigation Controller:
Screen Shot 2016-03-16 at 12.06.25 PM
Выбрав фотографа photographer, мы задаем для Модели destinationViewController, только фотографии photos, сделанные этим фотографом.
Константы для storyboard собраны в private структуре Storyboard:
FlickrPhotographersTVС.swift
Screen Shot 2016-03-16 at 12.07.56 PM
Точно также, как и в “классическом варианте” будем считать, что класс FlickrPhotographersTVC согласно концепции объектно-ориентированного программирования имеет более общий характер. Он не заботится о том, как получен массив [Photo]: c сервера Flickr или из NSUserDefaults. Для получения массива [Photo] c сервера Flickr у нас уже есть класс JustPostedFlickrPhotosTVC, который сделаем теперь subclass не FlickrPhotosTVC как в “классическом” варианте”, а subclass FlickrPhotographersTVC:
JustPostedFlickrPhotosTVС.swift
Screen Shot 2016-03-16 at 12.10.56 PM
Именно класс JustPostedFlickrPhotosTVC будет пользовательским классом для экранного фрагмента Flickr Photographers:
Screen Shot 2016-03-16 at 7.08.01 PM
а для экранного фрагмента Flickr Photos пользовательским классом будет FlickrPhotosTVC:
Screen Shot 2016-03-16 at 7.09.41 PM
Запускаем приложение, смотрим как оно работает. Все работает прекрасно, за исключением одной ситуации, когда iPhone 6+ (или iPhone 6s+) переходит из портретного режима в ландшафтный (то есть из collapsed режима в expanded согласно терминологии адаптивного Split View Controller). При этом его экран в портретном режиме показывает Список Фотографий на месте Detail, а вовсе не изображение выбранной фотографии:
Screen Shot 2016-03-16 at 7.11.38 PM
При переходе из collapsed режима, когда на экране находится только один View Controller, в режим expanded (Master и Detail одновременно на экране), адаптивный Split View Controller берет текущий экран в качестве Detail по умолчанию. А это совсем не то, что нам нужно: на месте Detail может быть только ImageViewController. С помощью другого метода separateSecondaryViewControllerFromPrimaryViewController делегата UISplitViewControllerDelegate нам самим придется произвести нужную настройку:
AppDelegate.swift
Screen Shot 2016-03-16 at 7.14.04 PM
Метод separateSecondaryViewControllerFromPrimaryViewController срабатывает при переходе из collapsed режима в expanded и он нас спрашивает, какой View Controller следует взять в качестве Detail. По умолчанию в качестве Detail берется текущий View Controller, если это не Master. В нашем особом случае, когда primaryViewController — таблица со списком фотографий FlickrPhotosTVC, вставленная в Navigation Controller, мы должны сгенерировать Detail в условиях скудной информации. Нам неоткуда взять ImageViewController (изображение фотографии) как только со storyboard и нам необходим идентификатор экранного фрагмента Detail на storyboard (в нашем случае это Navigation Controller для Detail). Пусть этим идентификатором будет “detailNavigation”:
Screen Shot 2016-03-16 at 7.16.37 PM
После восстановления Detail со storyboard, мы можем получить наш ImageViewController (в коде это controller) и сделать необходимые настройки: добавить кнопки на навигационную панель, выставить заголовок ImageViewController и настроить его Модель ( в нашем случае это imageURL), которая будет соответствовать первой строке в списке фотографий:
if let photo = photosView.photos.first {
controller.imageURL = NSURL(string: photo.imageURL)
controller.title = photo.title
}

Запускаем приложение, теперь все работает правильно. Кроме того, выделяется первая строка в списке фотографий, для которой показывается фотография:
Screen Shot 2016-03-16 at 7.18.30 PM
Вывод: для того, чтобы обеспечить адаптивную работу Split View Controller c многочисленные (>1) Table View Controllers в качестве Master, нужно использовать метод separateSecondaryViewControllerFromPrimaryViewController делегата UISplitViewControllerDelegate.
Код находится на Github — приложение AdaptiveSplitViewController2Swift.

Во второй части этого поста мы будем дальше усложнять наше экспериментальное приложение и распространим его на случай 3 — Tab Bar Controller в качестве Master для Split View Controller, и 5 — адаптивный Popover.
Код для всех вариантов можно найти на Github.

Можно скачать текст поста в PDF формате 

NewAdaptiveSplitViewControllerandPopoverCS193PWinter2015iOS9.pdf