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


Для ищущих


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


Комментарии (4)


  1. bonyadmitr
    25.07.2019 20:01
    +1

    Хорошо расписал, подчеркнул многие особенности UISplitViewController.


    (только для Xs Max и 8+)

    XR вроде тоже, и остальные "плюс" версии (6+, 7+)


    1. IvanVorobei Автор
      25.07.2019 20:07

      Может быть, не тестировал. Плюсы точно, да.


      Но привязываться не стоит — уже завтра эпл может изменить условия отображения сплита и увидим его на 5s)


      1. bonyadmitr
        25.07.2019 21:49
        +1

        Но привязываться не стоит

        Безусловно. Это так, для справки остальным, кто не в курсе вдруг.


        увидим его на 5s

        хотел бы посмотреть на это))


  1. georgeneversleep
    26.07.2019 16:26

    Вот вам отзыв:
    Я не понял в чем соль статьи. Разве мало в интернетах описания как приготовить UISplitViewController? Я еще года четыре назад множество статей видел по теме, даже на русском языке. А для работы с этим, достаточно простым компонентом, нужно прочитать всего лишь одну статью и пару вопросов/ответов на стэке.
    Без обид, но вам доставляет писать про тривиальные вещи для самых маленьких? В том плане что таких статей уже написано до вас.
    У вас же есть норм публикации про не самые тривиальные вещи.
    Про примеры реализации анимации в UIKit, например, статей мало не бывает. Про сторонние тулкиты тоже полезно, на мой взгляд.
    А вот про то как UITableView и UINavigationController использовать до вас по двадцать раз написано. Одно дело, если бы у вас структурированная серия статей была, где нужно все компоненты по порядку описать и показать примеры работы, но в ваших такой схемы не прослеживается. Да и таких публикаций тоже навалом.
    Я бы понял очередную статью про UICollectionViewDelegateFlowLayout, тут хоть десяток статей прочитай, все равно ничего не понятно. Но UISplitViewController, сами же написали — с 2014 года. Там уже несчетное количество статей написано на любом языке.
    Я к чему это — вижу у вас времени навалом и интерес писать имеется, так пишите что-то о чем еще не написано. Изложение у вас норм, воды нет, код, иллюстрации, все как у людей. Вашему исполнению бы, да такое же содержание и будет здорово.