Раньше для поддержки 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 вам не нужен.


Для ищущих


Ссылка на документацию, ссылка на гайдлайны.
Если вам удобнее смотреть видео, гляньте туториал:



Подписывайтесь на мой канал в телеграмме.