Раньше для поддержки iPad делали отдельный xib. Чтобы унифицировать лейаут, в 2014 году Apple представила Auto Layout и Size Classes, а для адаптивной навигации UISplitViewController.
Split-контроллер — это контейнер, который разместит два контроллера рядом. Слева будет навигационный контроллер (речь не про Navigation Controller), справа соответсвующий выбору в навигационном. Короче, как в Настройках.
Разберем как настроить UISplitViewController и его поведение на экранах.
Определяем контроллеры
Обозначим левый (навигационный) и правый контроллеры. Для обоих установим заголовок:
class MasterController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Master"
self.navigationController?.navigationBar.prefersLargeTitles = true
}
}
class DetailController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Detail"
self.navigationController?.navigationBar.prefersLargeTitles = true
}
}
Split-контроллер это контейнер для двух контроллеров. Как я писал до ката, слева навигационный-главный контроллер, справа соответствующий выбору в навигационном, или детальный (Detail Controller). Master и Detail запоминайте, слова будут встречаться в документации и протоколах.
Добавляем Split
Перейдем в AppDelegate, в методе didFinishLaunchingWithOptions создаем новое окно, инициализируем Split-контроллер и два других, устанавливаем в Split:
let masterController = MasterController()
let masterNavigationController = UINavigationController(rootViewController: masterController)
let detailController = DetailController()
let detailNavigationController = UINavigationController(rootViewController: detailController)
let splitViewController = UISplitViewController()
splitViewController.viewControllers = [masterNavigationController, detailNavigationController]
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window!.rootViewController = splitViewController
self.window!.makeKeyAndVisible()
Запустим симулятор. Портретная и альбомная ориентации соответсвенно:
В портертной ориентации не видно Master-контроллера. Смахните у левого края, чтобы он появился. Настраивается с помощью режимов, их разберем дальше.
Навигация
Добавим в Master-контроллер ячейки. Напомню, Master-контроллер это таблица, обернутая в UINavigationController.
class MasterController: UITableViewController {
override func viewDidLoad() {
self.tableView = UITableView(frame: .zero, style: .insetGrouped)
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "id")
self.navigationItem.title = "Master"
self.navigationController?.navigationBar.prefersLargeTitles = true
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "id")!
cell.textLabel?.text = "(indexPath)"
return cell
}
}
В iOS 13 появился новый стиль таблицы .insetGrouped, я установил его. Стиль доступен начиная с Xcode 11.
Запустим проект:
По нажатию на ячейку покажем детальный контроллер. Как же режут слух «детальные контроллеры», надеюсь в коментах подскажите перевод лучше. Код выглядит так:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let controller = DetailController()
controller.navigationTitle = "(indexPath)"
self.showDetailViewController(UINavigationController(rootViewController: controller), sender: nil)
}
Помните про нейминг мастер / детальный контроллеры? Вот пример использования Detail в методе showDetailViewController.
Внимательные заметят, что проперти navigationTitle у контроллера нет. Обновим класс Detail-контроллера:
class DetailController: UIViewController {
var navigationTitle: String = "Detail"
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = navigationTitle
self.view.backgroundColor = .white
}
}
Запустите проект и нажмите на ячейку:
Айфоны
Навигация адаптивная, а значит всё готово. Ну почти. Выберите айфон и запустите:
Работает как обычный Navigation-контроллер. Это универсальность адаптивность — в зависимости от свободного пространства Split-контроллер размещает главный и детальный контроллеры.
На айфоне первым открылся не Master-контроллер, а Detail. Переход от отображения двух контроллеров к одному похожая ситуация, настраивается делегатом UISplitViewControllerDelegate. Возвращаемое значение определяет показывать Master-контроллер, или Detail:
extension AppDelegate: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
Этот метод не всегда должен возвращать true. Пример: при смене ориентации в компактную и уже открытом Detail контроллере, может потребоваться оставить Detail на экране. Планируйте это поведение.
Альбомная для iPhone
Альбомная ориентация для айфонов работает без Split по умолчанию. Это исправляется режимом отображения для Split-контроллера:
splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
Задаем предпочтительный режим отображения, а именно показывать пару Master-Detail всегда, где это возможно. Возможность определяет API, настроить нельзя.
В портретной ориентации останется так же. А вот в альбомной (только для Xs Max и 8+):
Если в SDK изменятся условия для Split-контроллера, ваш проект по умолчанию реализует их.
Размеры
Можно настроить. Делается это отношением сторон:
splitViewController.preferredPrimaryColumnWidthFraction = 0.5
splitViewController.maximumPrimaryColumnWidth = 2000
Master и Detail будут одинаковых размеров. Обязательно установите maximumPrimaryColumnWidth. Опционально можно установить минимальную ширину. Скриншот добавлять не буду, и так много айпадов на туториал)
Прячем Master
Добавляем кнопку, которая открывает Detail на весь экран. Split-контроллер должен быть в режиме .allVisible. Вставьте код для Detail-контроллера в viewDidLoad:
if let splitController = self.splitViewController{
if let navController = splitController.viewControllers.last as? UINavigationController {
navController.topViewController?.navigationItem.leftBarButtonItem = splitController.displayModeButtonItem
}
}
Описать поведение сложно, а гифка получается размером с бюджет Москвы. Поэтому скриншот:
Или у меня в твиттере видео.
Гайдлайны
AutoLayout размещает элементы, Split-контроллер определяет навигацию. Эпл настоятельно рекомендует использовать статический Master-контроллер. Я ради эксперимента сделал слева навигационный контроллер, по нажатию на ячейку пушил контроллеры (вместо обновления Detail-контроллера). Выглядит странно.
In general, restrict navigation to one side of a split view. Placing navigation in both panes of a split view makes it hard for people to stay oriented and discern the relationship between the two panes.
Выделяйте активный выбор в Master-контроллере. Хотя содержимое Detail-контроллера может изменяться, оно всегда должно соответствовать выделению на Master. Это поможет людям контролировать отношения между контроллерами. Контролировать контроллеры — каламбур какой-то.
Не нужно использовать Split везде. Но если в вашем приложении сильная навигация, а root-контроллер это Tab или Navigation, скорее всего Split полезен. Если у вас одноэкранное приложение-переводчик, Split вам не нужен.
Для ищущих
Ссылка на документацию, ссылка на гайдлайны.
Если вам удобнее смотреть видео, гляньте туториал:
Автор: IvanVorobei